Search code examples
c++qtpointerspass-by-reference

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


In QT, I am using a QAction object to enable/disable menu buttons when the user clicks them. It occurred to me that I was writing the same functionality for every case so I turned it into a function with a private hash table that would do all of the controlling of the enabling and disabling of the menu buttons. In other words, my hash table looks like std::unordered_map<bool,QAction*> table. Where the boolean is the key and the object is the value I am updating. So I wrote the following function:

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;
  }
} 

So whenever I clicked on a menu item. I would call this function at the very top:

void on_action_menuItem1_triggered()
{
  updateButton(ui->actionDoAction);

  ...
}

I then realized I was passing the pointer by value. And since I have so many buttons I have to manage I don't want to be making any copies if I can avoid it. At this point I totally forgot I could do QAction *&currentButton to pass the pointer by reference. So I started looking around and I found std::reference_wrapper. So I then changed the function to:

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

and calling it by:

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

Is there any benefit to doing it this way rather than QAction *&currentButton?


Solution

  • Short Answer

    There is no benefit to passing by reference-to-pointer instead of by pointer unless you wish to modify the original pointer itself.

    Long Answer

    Based on the way your question is framed, as well as your first reply to comments to your question, you have a fundamental misunderstanding of what pointers and references actually are. Contrary to what you seem to believe, they are not the objects themselves.

    Lets go back to the fundamentals a bit.

    There are two main areas in memory we have access to in order to store data, the Stack memory and the Heap memory.

    When we declare variables, we get allocated space in Stack memory and we can access the data by using the variable names. The memory gets automatically de-allocated when the variable falls out of scope.

    void myFunction() {
        MyClass instance;         // allocated in Stack memory
        instance.doSomething();
    
    }   // instance gets automatically de-allocated here
    

    The biggest problem is that the amount of Stack memory is extremely limited compared to normal program needs and the fact that you often need data to persist outside certain scopes that creating instances of large classes in Stack memory is usually a bad idea. That's where the Heap becomes useful.

    Unfortunately, with Heap memory, you need to take over the lifetime of the memory allocation. You also don't have direct access to data in Heap memory, you need a kind of stepping-stone to get there. You must explicitly ask the OS for a memory allocation, and then explicitly tell it to de-allocate the memory when you're done. C++ give us two operators for this: new and delete.

    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  
    }
    

    As you clearly seem to understand, in this case instance is known as a pointer. What you don't seem to realise is that a pointer is not an instance of an object itself, it is the "stepping stone" TO the object. The purpose of a pointer is to hold that memory address so we don't lose it and enable us to get to that memory by de-referencing the memory location.

    In C++ there are two ways of doing this: you either de-reference the entire pointer, then access the object's members just like you would an object on the Stack; or, you would use the Member Dereferencing operator and access the member using that.

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

    Pointers themselves are merely special-instances of integers. The values they contain are the memory addresses in the Heap memory where you allocate your objects. Their size depends on the platform you have compiled for (usually 32-bit or 64-bit) so passing them around is nothing more expensive than passing an integer around.

    I cannot stress it enough that pointer variables are not the objects themselves, they are allocated in Stack memory and behave exactly like any other stack variable when they go out of scope.

    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'
    

    Now, because under-the-hood, pointers are nothing more than special-purpose integers, passing them around as no more expensive than passing any other kind of integer.

    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
    

    So far, I have not mentioned references, because they are by-and-large the same as pointers. They are also just integers under-the-hood but they simplify usage by making the member-access syntax the same as that of Stack variables. Unlike ordinary pointers, references have to be initialised with valid memory location and that location cannot be changed.

    The following are functionally equivalent:

    MyClass &instance
    MyClass * const instance
    

    References to pointers are double-indirections, they're essentially pointers to pointers and are useful if you want to be able to manipulate, not only the Heap object, but also the pointer containing the memory location to that heap object.

    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
    

    If I have explained myself clearly, and you have followed me so far, you should now understand that, in your case, when you pass a pointer to QAction to a function, you're not copying the QAction object, you're only copying the pointer to that memory location. Since pointers are merely integers under the hood, you're only copying something that's either 32-bits or 64-bits in size (depending on your project settings), and changing that to a reference-to-pointer will make absolutely no difference.