Ways to pass Arguments in C++

In C++ there are many ways to pass an argument to a function. The following code shows some commonly used variants. It is also possible to add the const keyword at different places when passing by pointers to make either the pointer const or the value the pointer is pointing at const, or both.

You can also pass a reference to a pointer or a pointer to a pointer. I won’t cover these in detail but it is worth knowing that they do occur.

// Pass by value
void foo(Bar bar);
// Pass by reference
void foo(Bar& bar);
// Pass by const reference
void foo(const Bar& bar);
// Pass by rvalue reference
void foo(Bar&& bar);
// Pass by pointer
void foo(Bar* bar);

With all these possibilities it can be hard to know when to use which. Let’s try to find some use case for each of the examples above.

Pass by value

When you pass an argument by value a copy of the original value will be created and supplied to the function. Creating this copy is expensive for large objects but there are at least two use cases where pass by value is the best fit.

Passing primitive data types

Examples of primitive data types are bool, char, int, long, float, and double. These are small and cheap to copy and there is no winning in creating a reference or a pointer to them instead of just creating a copy of the actual value.

Passing enum values

Enumerations, or enums for short are usually represented by integers that can be passed by value without any extra overhead.

enum class Color {
  Red,
  Green,
  Blue,
  Black,
  White
};

void setBackground(Color color) {
  // Some implementation goes here...
}

int main() {
  setBackground(Color::Green);
  // ...
}

Passing movable types for copying

This use case is a bit trickier to explain. From C++ 11 a type can be movable. This means that the ownership of its underlying data can be transferred – moved – to another object. One commonly used type that is movable it std::string. Without going into move semantics and rvalue references we can just say that moving the ownership of the data is in many cases a lot cheaper than creating a copy and freeing up the original memory.

Now, since passing by value will create a copy it is possible to use this fact in cases where you would want to create a copy anyway and then move the ownership of the data in the copy to the new object. Sounds confusing? Let’s take a look at an example:

class Person {
std::string m_name;
public:
  Person(std::string name)
    : m_name{ std::move(name) } {}
};

Here a std::string is passed in to the constructor by value, then the ownership of the underlying data is transferred from the the argument name to the member variable m_name. If you are a seasoned C++ developer you might think that the correct way to do this would be to pass name as const reference instead. That would however not take advantage of the fact that a std::string is movable which allows for some optimization. An example:

int main() {
  std::string name{ "John" };
  Person p{ name }; // Creates a copy of 'name' as argument
  Person p2{ "Danny" }; // Optimized. No copy is created.
};

In the creation of p2 in the code above the compiler will be able to generate optimized code that does not create any copy of ”Danny”. In order for the same code and optimization to be possible using references two constructors would be needed, one taking the argument as const reference and another taking the argument as an rvalue reference.

Pass by reference

This option is to be used when you need to somehow modify the argument passed in, and let the caller have access to the modified object. I try to avoid using this since it can be unclear when reading the calling code that the object passed in is actually being changed. Still there might be some cases where you want to do this. My advice would then be to name the functions so that it is clear that the object is changed:

enum class CreditScores {
  Low,
  Medium,
  High
}

void setCreditScoreOn(Person& p) {
  p.creditScore = CreditScores.Low;
}

int main() {
  Person dylan{ "Dylan" };
  setCreditScoreOn(dylan); // Should be obvious that dylan is modified
  if (dylan.creditScore == CreditScores.Low) {
    std::cout << "Loan rejected\n";
  }
}

Pass by const reference

This option should be the default way of passing user defined types that you do not wish to modify or copy, or user defined types that you do wish to copy but are not movable. Note that user defined types does include a lot of types in the standard library such as std::string, std::vector, std::map, and so on. The exception from the rule is enums. These can be passed by value since they are represented by a primitive data type.

std::string getFullName(const Person& p) {
  return p.firstName() + " " + p.lastName();
}

By using the const keyword the compiler can apply certain optimizations since it ”knows” that the Person object will not be modified when getFullName() is called.

Pass by rvalue reference

The double ampersand, &&, identifies an rvalue reference. An rvalue is a value that does not have a name, and therefore you can’t get the address of it or put it on the left side of an assignment:

int n{3}; // 3 is an rvalue and n is an lvalue. Since n has a name
n = 4;    // it can be put on the left side of an assignment

But if something does not have a name, how do you add it as an argument to a function? Well, you define it directly in the argument list, like this:

void print(const std::string& str);  // Normal reference
void print(const std::string&& str); // R-value reference

int main() {
  std::string str{ "Hello" };
  print(str); // Invokes the first print function
  print(std::string{ ", World!" }); // Invokes the 2:nd print function
}

The example above works but it is not something that you would normally do. What rvalue references really do is to enable move semantics. I will not go into details of move semantics here since it is a large topic that requires a post of its own, but I will mention that it is possible to cast a value to an rvalue reference with std::move.

Pass by pointer

In most cases passing raw pointers around should be avoided in modern C++. The reason for avoiding them is that it is easy to make mistakes – like forgetting to check if the pointer is a nullptr, accidentally de-referencing freed memory, or reading or writing outside of the allocated memory – which can lead to really nasty bugs that are next to impossible to find. With that said, there are still some use cases where pointers are needed. But when choosing between using a reference or a pointer, use references when you can and pointers when you have to.

One case when you might need to use a pointer is when interacting with a library written in C (there are no references in C) or when you have a need to indicate a special condition which you can do with a nullptr. In the case of a raw pointer without any const you are free to alter both the pointer and the object that the pointer is pointing to:

void zeroUntil(int* values, int sentinel) {
  if (!values) return;
  while (*values != sentinel) {
    *values = 0;
    values++; // Note that 'values' here is a copy of the pointer in the calling code. Incrementing it has no effect on the pointer in the caller.
  }
}

int main() {
  int values[]{ 1, 2, 3, 4, 5, 6, 7 };
  zeroUntil(values, 5);
  // values is [0, 0, 0, 0, 5, 6, 7]
}

Adding const either restricts modification of the pointer or the value it points to:

void foo(const Bar* bar); // bar is a pointer to a Bar object that is const
void foo(Bar* const bar); // bar is a const pointer to a Bar object that is not const
void foo(const Bar* const bar); // bar is a const pointer to a Bar object that is const

Ending words

Hopefully this article gave you a good overview of when to use pass by value, pass by reference, and pass by pointer. It does not cover all variants that you might encounter but enough to get a good understanding of the different possibilities.

Live long and code well!

Lämna en kommentar

Din e-postadress kommer inte publiceras. Obligatoriska fält är märkta *