- Function definitions
- Parameter passing: in and out
- Parameter passing: ownership semantics
- Value return semantics
- Other functions
- Related rules
Parameter passing: in and out
The C++ Core Guidelines have a few rules to express various ways to pass parameters in and out of functions.
The first rule presents the big picture. First, it provides an overview of the various ways to pass information in and out of a function (see Table 4.1).
Table 4.1 Normal parameter passing
Cheap to copy or impossible to copy |
Cheap to move or moderate cost to move or don’t know |
Expensive to move |
|
---|---|---|---|
In |
func(X) |
func(const X&) |
|
In & retain “copy” |
|||
In/Out |
func(X&) |
||
Out |
X func() |
func(X&) |
The table is very concise: The headings describe the characteristics of the data regarding the cost of copying and moving. The rows indicate the direction of parameter passing.
Kind of data
Cheap to copy or impossible to copy: int or std::unique_ptr
Cheap to move: std::vector<T> or std::string
Moderate cost to move: std::array<std::vector> or BigPOD (POD stands for Plain Old Data—that is, a class without constructors, destructors, and virtual member functions.)
Don’t know: template
Expensive to move: BigPOD[] or std::array<BigPOD>
Direction of parameter passing
In: input parameter
In & retain “copy”: caller retains its copy
In/Out: parameter that is modified
Out: output parameter
A cheap operation is an operation with a few ints; moderate cost is about one thousand bytes without memory allocation.
These normal parameter passing rules should be your first choice. However, there are also advanced parameter passing rules (see Table 4.2). Essentially, the case with the “in & move from” semantics was added.
Table 4.2 Advanced parameter passing
Cheap to copy or impossible to copy |
Cheap to move or moderate cost to move or don’t know |
Expensive to move |
|
---|---|---|---|
In |
func(X) |
func(constX&) |
|
In & retain “copy” |
|||
In & move from |
func(X&&) |
||
In/Out |
func(X&) |
||
Out |
X func() |
func(X&) |
After the “in & move from” call, the argument is in the so-called moved-from state. Moved-from means that it is in a valid but not nearer specified state. Essentially, you have to initialize the moved-from object before using it again.
The remaining rules to parameter passing provide the necessary background information for these tables.
The rule is straightforward to follow. Input values should be copied by default if possible. When they cannot be cheaply copied, take them by const reference. The C++ Core Guidelines give a rule of thumb to the question, Which objects are cheap to copy or expensive to copy?
You should pass a parameter par by value if sizeof(par) < 3 * sizeof(void*).
You should pass a parameter par by const reference if sizeof(par) > 3 * sizeof(void*).
void f1(const std::string& s); // OK: pass by reference to const; // always cheap void f2(std::string s); // bad: potentially expensive void f3(int x); // OK: unbeatable void f4(const int& x); // bad: overhead on access in f4()
This rule stands for a special input value. Sometimes you want to forward the parameter par. This means an lvalue is copied and an rvalue is moved. Therefore, the constness of an lvalue is ignored and the rvalueness of an rvalue is preserved.
The typical use case for forwarding parameters is a factory function that creates an arbitrary object by invoking its constructor. You do not know if the arguments are rvalues nor do you know how many arguments the constructor needs.
// forwarding.cpp #include <string> #include <utility> template <typename T, typename ... T1> // (1) T create(T1&& ... t1) { return T(std::forward<T1>(t1)...); } struct MyType { MyType(int, double, bool) {} }; int main() { // lvalue int five=5; int myFive= create<int>(five); // rvalues int myFive2= create<int>(5); // no arguments int myZero= create<int>(); // three arguments; (lvalue, rvalue, rvalue) MyType myType = create<MyType>(myZero, 5.5, true); }
The three dots (ellipsis) in the function create (1) denote a parameter pack. We call a template using a parameter pack a variadic template.
The combination of forwarding together with variadic templates is the typical creation pattern in C++. Here is a possible implementation of std::make_unique<T>.
template<typename T, typename... Args> std::unique_ptr<T> make_unique(Args&&... args) { return std::unique_ptr<T>(new T(std::forward<Args>(args)...)); } std::make_unique<T> creates a std::unique_ptr for T
The rule communicates its intention to the caller: This function modifies its argument.
std::vector<int> myVec{1, 2, 3, 4, 5}; void modifyVector(std::vector<int>& vec) { vec.push_back(6); vec.insert(vec.end(), {7, 8, 9, 10}); }
The rule is straightforward. Just return the value, but don’t use a const value because it has no added value and interferes with move semantics. Maybe you think that copying a value is an expensive operation. Yes and no. Yes, you are right, but no, the compiler applies RVO (Return Value Optimization) or NRVO (Named Return Value Optimization). RVO means that the compiler is allowed to remove unnecessary copy operations. What was a possible optimization step becomes in C++17 a guarantee.
MyType func() { return MyType{}; // no copy with C++17 } MyType myType = func(); // no copy with C++17
Two unnecessary copy operations can happen in these few lines, the first in the return call and the second in the function call. With C++17, no copy operation takes place. If the return value has a name, we call it NRVO. Maybe you guessed that.
MyType func() { MyType myValue; return myValue; // one copy allowed } MyType myType = func(); // no copy with C++17
The subtle difference is that the compiler can still copy the value myValue in the return statement according to C++17. But no copy will take place in the function call.
Often, a function has to return more than one value. Here, the rule F.21 kicks in.
When you insert a value into a std::set, overloads of the member function insert return a std::pair of an iterator to the inserted element and a bool set to true if the insertion was successful. std::tie with C++11 or structured binding with C++17 are two elegant ways to bind both values to a variable.
// returnPair.cpp; C++17 #include <iostream> #include <set> #include <tuple> int main() { std::cout << '\n'; std::set<int> mySet; std::set<int>::iterator iter; bool inserted = false; std::tie(iter, inserted) = mySet.insert(2011); // (1) if (inserted) std::cout << "2011 was inserted successfully\n"; auto [iter2, inserted2] = mySet.insert(2017); // (2) if (inserted2) std::cout << "2017 was inserted successfully\n"; std::cout << '\n'; }
Line (1) uses std::tie to unpack the return value of insert into iter and inserted. Line (2) uses structured binding to unpack the return value of insert into iter2 and inserted2. std::tie needs, in contrast to structured binding, a predeclared variable. See Figure 4.2.
Figure 4.2 Returning a std::pair