在指针上使用 reference_wrapper 与通过引用传递指针有啥好处?

Posted

技术标签:

【中文标题】在指针上使用 reference_wrapper 与通过引用传递指针有啥好处?【英文标题】:Is there a benefit to using reference_wrapper on a pointer vs passing the pointer by reference?在指针上使用 reference_wrapper 与通过引用传递指针有什么好处? 【发布时间】:2019-03-21 18:17:15 【问题描述】:

在 QT 中,我使用QAction 对象在用户单击菜单按钮时启用/禁用它们。我突然想到,我为每种情况编写了相同的功能,所以我把它变成了一个带有私有哈希表的函数,它可以控制菜单按钮的启用和禁用。换句话说,我的哈希表看起来像std::unordered_map<bool,QAction*> table。布尔值是键,对象是我要更新的值。于是我写了如下函数:

void updateButton(QAction *currentButton)

  if(!table.empty())
  
    // Enable the last menu item you clicked on and disable the current menu item.
    table[false]->setEnabled(true);
    currentButton->setEnabled(false);
    table[false] = currentButton;
  
  else
  
    // Menu item was clicked for the first time
    currentButton->setEnabled(false);
    table[false] = currentButton;
  
 

所以每当我点击一个菜单项时。我会在最顶层调用这个函数:

void on_action_menuItem1_triggered()

  updateButton(ui->actionDoAction);

  ...

然后我意识到我正在按值传递指针。而且由于我有很多按钮我必须管理,如果可以避免的话,我不想制作任何副本。在这一点上,我完全忘记了我可以做QAction *&currentButton 来通过引用传递指针。所以我开始环顾四周,我发现 std::reference_wrapper。于是我又把函数改成:

void updateButton(std::reference_wrapper<QAction*> currentButton)

   ...

并通过以下方式调用它:

updateButton(std::ref(ui->actionDoAction));

这样做比QAction *&amp;currentButton 有什么好处吗?

【问题讨论】:

指针是小对象,复制起来很便宜,你可以按值传递。 std::reference_wrapper 在木头下也是一个指针。 @Sailanarmo 按值传递指针不会复制对象,它只会复制指针。 “我正在修改指针中的状态” - 我对此表示怀疑。您可能正在修改指针 指向 的内容,但如果您正在修改指针变量本身的某些部分,我会感到非常惊讶。请具体准确在你用来描述你的行动的话。 @Sailanarmo 通过引用传递指针的唯一原因是如果您想修改指针本身(它在调用者中指向什么)。所以没有。 【参考方案1】:

简答

除非您希望修改原始指针本身,否则通过引用指针而不是指针传递没有任何好处。

长答案

根据您提出问题的方式,以及您对 cme​​ts 的第一次回复,您对指针和引用的实际含义存在根本性的误解。与您似乎相信的相反,它们本身并不是物体。

让我们回到基本原理。

为了存储数据,我们可以访问内存中的两个主要区域,堆栈内存和堆内存。

当我们声明变量时,我们在堆栈内存中分配了空间,我们可以通过使用变量名来访问数据。当变量超出范围时,内存会自动释放。

void myFunction() 
    MyClass instance;         // allocated in Stack memory
    instance.doSomething();

   // instance gets automatically de-allocated here

最大的问题是堆栈内存的数量与正常程序的需求相比极为有限,而且您经常需要数据在某些范围之外持续存在这一事实,因此在堆栈内存中创建大型类的实例通常是一个坏主意。这就是堆变得有用的地方。

不幸的是,使用堆内存,您需要接管内存分配的生命周期。您也无法直接访问堆内存中的数据,您需要一种垫脚石才能到达那里。您必须明确要求操作系统分配内存,然后在完成后明确告诉它取消分配内存。 C++ 为此提供了两个运算符:newdelete

void myFunction()
   MyClass *instance = new MyClass();  // ask OS for memory from heap
   instance->doSomething();
   delete instance;                    // tell OS that you don't need the heap memory anymore  

您似乎清楚地理解,在这种情况下,实例称为pointer。您似乎没有意识到指针不是对象本身的实例,它是对象的“垫脚石”。指针的目的是保存该内存地址,这样我们就不会丢失它,并使我们能够通过取消引用内存位置来访问该内存。

在 C++ 中有两种方法:要么取消引用整个指针,然后像访问堆栈上的对象一样访问对象的成员;或者,您将使用 Member Dereferencing 运算符并使用它访问成员。

void myFunction()
    MyClass *instance = new MyClass();
    (*instance).doSomething();          // older-style dereference-then-member-access
    instance->doSomethingElse();        // newer-style using member dereference operator

指针本身只是整数的特殊实例。它们包含的值是您分配对象的堆内存中的内存地址。它们的大小取决于您编译的平台(通常是 32 位或 64 位),因此传递它们并不比传递整数更昂贵。

我怎么强调指针变量不是对象本身,它们被分配在堆栈内存中并且当它们超出范围时的行为与任何其他堆栈变量完全一样。

void myFunction() 
    MyClass *instance = new MyClass();         // pointer-sized integer of type 'pointer-to-MyClass' created in Stack memory
    instance->doSomething();
  // instance is automatically de-allocated when going out of scope. 
// Oops! We didn't explicitly de-allocate the object that 'instance' was pointing to 
// so we've lost knowledge of it's memory location. It is still allocated in Heap 
// memory but we have no idea where anymore so that memory is now 'leaked'

现在,因为在底层,指针只不过是特殊用途的整数,传递它们并不比传递任何其他类型的整数更昂贵。

void myFunction()
    MyClass *instance = new MyClass();  // 'instance' is allocated on the Stack, and assigned memory location of new Heap allocation
    instance->doSomething();
    AnotherFunction(instance);
    delete instance;                    // Heap memory pointed to is explicitly de-allocated
 // 'instance' is automatically de-allocated on Stack

void anotherFunction(MyClass *inst) // 'inst' is a new pointer-to-MyClass on the Stack with a copy of the memory location passed in
    inst->doSomethingElse();
 // 'inst' is automatically de-allocted

到目前为止,我还没有提到引用,因为它们与指针大体上相同。它们也只是底层的整数,但它们通过使成员访问语法与堆栈变量的语法相同来简化使用。与普通指针不同,引用必须使用有效的内存位置进行初始化,并且该位置不能更改。

以下功能等效:

MyClass &instance
MyClass * const instance

对指针的引用是双重间接的,它们本质上是指向指针的指针,如果您希望能够操作,不仅是堆对象,还包括包含指向该堆对象的内存位置的指针。

void myFunction()
    QString *str = new QString("First string");  // str is allocated in Stack memory and assigned the memory location to a new QString object allocated in Heap memory
    substituteString(str);                       
    delete str;                                  // de-allocate the memory of QString("Second String"). 'str' now points to an invalid memory location
 // str is de-allocated automatically from Stack memory

void substituteString(QString *&externalString)  // 'externalString' is allocated in Stack memory and contains memory location of 'str' from MyFunction()
   delete externalString;                        // de-allocate the Heap memory of QString("First string"). 'str' now points to an invalid location
   externalString = new QString("Second string"); // Allocate new Heap memory for a new QString object. 'str' in MyFunction() now points to this new location
 // externalString is de-allocated automatically from Stack memory

如果我已经清楚地解释了自己,并且到目前为止您一直关注我,那么您现在应该明白,在您的情况下,当您将指向 QAction 的指针传递给函数时,您并没有复制 QAction对象,您只是将指针复制到该内存位置。由于指针只是底层的整数,因此您只是复制大小为 32 位或 64 位的东西(取决于您的项目设置),并将其更改为引用指针绝对没有区别。

【讨论】:

这是迄今为止最有帮助的答案,清楚地解释了我所困惑的一切。【参考方案2】:

您的问题有几个不同的部分,我将分别解决每个部分。

传递指针的代价有多大?

传递指针基本上是免费的。如果您传递给它的函数没有太多参数,那么编译器会将指针传递到 CPU 寄存器中,这意味着指针的值甚至不必复制到堆栈中。

通过引用传递有多昂贵?

通过引用传递某些东西有一些好处。

引用允许您假定引用指向一个有效对象(因此您不必检查引用的地址是否为空)。 当您通过引用传递时,您可以像使用普通对象一样使用它;无需使用-&gt; 语法。 编译器可以(有时)生成更快的程序,因为它知道无法重新分配引用。

但是,在底层传递引用的方式与传递指针的方式相同。两者都只是地址。而且两者基本上都是免费的。

你应该使用哪一个?

如果您要传递一个您不需要需要修改的大对象,请将其作为 const 引用传递。 如果您要传递一个您不需要需要修改的小对象,请按值传递它 如果您要传递一个您确实需要修改的引用,请通过引用传递它。

你在修改什么? (按钮,还是指向按钮的指针?)

如果要修改按钮,通过引用传递QAction:

void pushButton(QAction& currentButton) 
    currentButton.push(); 

如果要修改按钮的指针,请通过引用传递指针。这是一种不寻常的情况,我不明白您为什么要修改指向按钮的指针。

void modifyPointer(QAction*& buttonPointer) 
    buttonPointer++; //I dunno, increment the pointer

更好的是,只返回修改后的指针:

QAction* modifyPointer(QAction* buttonPointer) 
    return buttonPointer + 1;

【讨论】:

【参考方案3】:

如果您将指针作为参数传递,几乎在所有情况下,您都希望按值传递它们。性能将是相同的,因为每个主要的 c++ 编译器在内部都将引用视为指针。

或者你是说你认为当你通过值传递一个指针时会创建一个 OBJECT COPY ?事实并非如此 - 您可以随意传递指针,不会分配新对象。

【讨论】:

以上是关于在指针上使用 reference_wrapper 与通过引用传递指针有啥好处?的主要内容,如果未能解决你的问题,请参考以下文章

多态对象的数组

是否可以仅从 std::any 使用 std::reference_wrapper 创建 std::any?

std::reference_wrapper 中的 Visual C++ 10.0 错误?

展开 std::reference_wrapper 的成本

非模板 std::reference_wrapper 赋值运算符和模板构造函数

我可以实例化一个 std::reference_wrapper<T> ,其中 T 是不完整的类型吗?