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

Is there a benefit to using reference_wrapper on a pointer vs passing the pointer by reference?

在 QT 中,当用户单击菜单按钮时,我将 QAction 对象用于 enable/disable 菜单按钮。我突然想到,我为每种情况都编写了相同的功能,所以我将它变成了一个带有私有散列 table 的函数,该函数将对菜单按钮的启用和禁用进行所有控制。换句话说,我的散列 table 看起来像 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 *&currentButton 有什么好处吗?

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

传递一个指针有多昂贵?

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

通过引用传递有多昂贵?

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

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

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

你应该使用哪一个?

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

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

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

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

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

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

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

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

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

或者您是说您认为按值传递指针时会创建 OBJECT COPY?事实并非如此——你可以随心所欲地传递一个指针,不会分配新的对象。

简答

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

长答案

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

让我们回到基础。

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

当我们声明变量时,我们会在 Stack 内存中分配 space,我们可以使用变量名访问数据。当变量超出范围时,内存会自动取消分配。

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

}   // instance gets automatically de-allocated here

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

不幸的是,对于堆内存,您需要接管内存分配的生命周期。您也无法直接访问堆内存中的数据,您需要一种垫脚石才能到达那里。您必须明确要求 OS 进行内存分配,然后明确告诉它在完成后取消分配内存。 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。您似乎没有意识到的是,指针不是对象本身的实例,它是对象的 "stepping stone" 。指针的目的是保存该内存地址,这样我们就不会丢失它,并使我们能够通过取消引用内存位置来访问该内存。

在 C++ 中,有两种方法可以做到这一点:要么取消对整个指针的引用,然后像访问 Stack 上的对象一样访问对象的成员;或者,您将使用 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

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

以下在功能上是等效的:

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 位大小的东西(取决于您的项目设置),并将其更改为指针引用绝对没有区别。