C++ Pass by Value, Pointer*, &Reference

A while back, I was working with my college friends on a school project involving C++ function parameters. We were working with a function that took parameters by reference and my friend thought that meant the same as passing parameters by pointer. When he found out they were different, he said the exact same thing that I have heard many other people say:

?Wait! Passing by pointer and passing by reference are different??

I have heard it so much so that I have written an article explaining the difference between passing by value, reference, and pointer.

Passing by Value

Passing by value is the most straightforward way to pass parameters. When a function is invoked, the arguments are copied to the local scope of the function. For example,

// class declarationclass Foo {};void PassByValue(Foo f, int n){ // Do something with f and n.}int main(){ Foo foo; int i = 1; PassByValue(foo, i);}

When PassByValue is invoked in main()?s body, a copy of foo and i is given to PassByValue (copied into PassByValue?s stack frame). This works well for primitive types like ints, floats, pointers, and small classes, but it doesn?t work well for big types. Think std::strings from the STL or if Foo was a really big class with a lot of member variables. Copying these objects would be inefficient if the function doesn?t do a lot of work on these objects. The majority of the CPU time for the function call would be spent on copying the arguments.

Another issue is that in the function call, the program is operating on a copy of the Foo object, not the actual foo object from main(). Any mutations on the object will not be visible in the main() function after PassByValue() completes because all of the work will be discarded once the PassByValue function ends.

These issues can be addressed by passing by pointer or passing by reference.

Pass by Pointer

A pointer is a special type of object that has the memory address of some object. The object can be accessed by dereferencing (*) the pointer, which knows the type of object it is pointing to. Since pointers are memory addresses, they are only 32 or 64 bits so they take up at most 8 bytes. Let?s revisit the previous example but pass Foo by pointer instead.

class Foo { public: int data[100];};void PassByPointer(Foo* f, int n){ // Do something with *f and n.}int main(){ Foo foo; int i = 0; PassByPointer(&foo, i); return 0;}

Here, we are passing &foo as the first argument to PassByPointer(). The & operator means ?address of? and it is called the address-of operator. Simple, right? It gets more confusing because the & can also denote a reference variable, it depends on the context of where the & is. I will explain this in more detail in the Pass by Reference section later, and it will be clearer once you see an example.

So now, foo is not really being copied into the function. The pointer to foo is being copied, and as state above, this is only 8 bytes on most modern computing architectures. This is not a lot of data and can be done in one instruction cycle. Now PassByPointer() can access foo?s member variables by using the dereference operator.

All modifications to the object will also be visible in the main() function after PassByPointer() ends. This may seem like the perfect solution, but it does have its drawbacks.

One drawback with passing by pointer is the lifetime of the pointed to data. The memory location containing the object can change at any time. A common scenario is when the memory location of the object is updated or deleted by another thread. For example, thread A might delete foo and thread B might try to read foo. Depending on the order of execution of threads A and B, there could be a read-after-delete situation, or everything could be fine if the read happens before the delete. This is called a race condition. When multiple threads are accessing the same resource, concurrency primitives such as mutexes must be designed into the program.

There is also a special pointer called nullptr which has value 0x0 and, as the name suggests, points to nothing. Trying to dereference (read from) it will lead to a dreaded segmentation fault. Since nullptr is allowed wherever a function accepts a pointer type, PassByPointer() could be called like PassByPointer(nullptr, 0); . Now, PassByPointer(), as well as every function that has a pointer parameter, needs to do a nullptr check to make sure that it received a valid pointer before dereference it.

void PassByPointer(Foo* f, int n){ if(f == nullptr) return; // Do something with *f and n.}

As you can see, this can be very tedious and it?s very easy to miss a function in a large codebase.

Another drawback is the concept of ownership. This mostly applies to objects allocated on the heap (i.e. dynamic memory or free store). For example,

int main(){ Foo* foo = new Foo(); int n = 0; PassByPointer(foo, n); // Do I delete foo here? Or will PassByPointer do it? // delete foo; return 0;}

As you may already know, objects allocated on the heap need to be freed before the program ends. In this case, who is responsible for deleting Foo? Is it main()? Or is it PassByPointer()? A programmer would have to look at the implementation of PassByPointer() or maybe the documentation of the function if it is been kept up to date (that is a very big if). IfPassByPointer() is taken from a library, the user of the library shouldn?t have to jump around the implementation details to figure out who is responsible for deleting the heap allocated object. Even worse, if the library writers decide to switch the function?s responsibility from not deleting the pointer to deleting the pointer or vice versa, the library user?s code will have a double delete error or memory leak. Nonetheless to say, pointers are tricky but there is hope. In C++11?s standard template library, there is something called a unique pointer which encapsulates the concept of ownership and avoids these problems with ?raw? pointers. Unique pointers require knowledge of move semantics and rvalues which I will not cover here. For more information about unique pointers, check out https://en.cppreference.com/w/cpp/memory/unique_ptr

Pass by Reference

Last but not least, we have pass by reference. Think of a reference variable as another alias for an object. It is a second variable name for the same object. There is no new memory being consumed (except for the variable name) and all operations on the reference variable affect the real underlying object being referred to. For example,

int count = 0;int& count_ref = count;count++;count_ref++;// count is now 2

This is powerful because it allows us to pass large objects into functions by giving another name to the object and not copying the large object, while also allowing us avoid the hairy complications of passing by pointer. A reference variable can never point to null so there doesn?t need to be a null check in every function, and since a reference variable is not heap allocated, there is no worry of a memory leak or figuring out who is responsible for owning and deleting the object.

class Foo { public: int data[100];};void PassByReference(Foo& f, int n){ // Do something with f and n.}int main(){ Foo foo; int i = 0; PassByReference(foo, i); return 0;}

Now anything that PassByReference() does to Foo will be still visible in main() after the function ends, but no large objects were copied so the overhead is negligible. Only a reference variable f was created to refer to the foo . Note here that the ampersand does not mean the address-of operator used to retrieve a pointer to an object. Here, it specifies that the variable is a reference variable that refers to an object that already exists. For you C++ language lawyers out there, the address-of operator (&) is an operator that can only be applied to an lvalue, whereas the reference ampersand is used in a declaration specifier sequence.

A lot more is going on behind the scenes in passing by value, pointer, and reference. I have only covered the tip of the iceberg. There are some edge cases and obscure pitfalls that I believe were not common enough to fall into this article, but if you find this content interesting and would like me to write more in-depth explanations and tutorials about C++ in general, please let me know!

10

No Responses

Write a response