C++ Core Guidelines: The Rules for in, out, in-out, consume, and forward Function Parameter

You have many choices to pass function parameters. You can pass by value or by reference. A reference can be const or non-const. You can even move or forward your parameters.  Your decision should depend on if it is an in, an out, an in-out, a consume, or a forward function parameter. Curious? Read the post!

 

According to the guidelines, let's talk about in, out, in-out, consume, or forward parameter.

Parameter passing expression rules:

Seems to be a lot of stuff, but bear with me. The first rule F.15 summarises the guidelines F.16 - F.21

F.15: Prefer simple and conventional ways of passing information

Here is the big picture from the C++ core guidelines. These are the normal parameter passing rules.

FunctionParameters

Based on this rules there are a few additions in green, the so-called advanced parameter passing rules.

FunctionParametersAdvanced

 

The rationale for the rules and their variations will follow in the next rules.

F.16: For “in” parameters, pass cheaply-copied types by value and others by reference to const

This rule for in parameters is straightforward and so is the example:

void f1(const string& s);  // OK: pass by reference to const; always cheap

void f2(string s);         // bad: potentially expensive

void f3(int x);            // OK: Unbeatable

void f4(const int& x);     // bad: overhead on access in f4()

 

I often hear the question in my seminars: What means cheaply copyable? The guidelines are quite concrete.

  • You should not  copy the parameter p if sizeof(p) > 4 * sizeof(int)
  • You should not use a const reference to p if sizeof(p) < 3 * sizeof(int)

 I assume these numbers are based on experience.

F.17: For “in-out” parameters, pass by reference to non-const

In-out parameters will be modified in the function, so using a non-const reference makes sense.

void appendElements(std::vector<int>& vec){
  // append elements to vec
  ...
}

F.18: For “consume” parameters, pass by X&& and std::move the parameter

This is the first advanced rule to consume parameters. Use a rvalue reference if you consume the parameter and move it inside the function body. Here is an example:

void sink(vector<int>&& v) {   // sink takes ownership of whatever the argument owned
    // usually there might be const accesses of v here
    store_somewhere(std::move(v));
    // usually no more use of v here; it is moved-from
}

 

There is an exception to this rule. std::unique_ptr is a move-only type that is cheap to move, therefore, you can move it.

void sink(std::unique_ptr<int> p) { 
... }
...
sink(std::move(uniqPtr));

 

F.19: For “forward” parameters, pass by TP&& and only std::forward the parameter

This is the idiom that factory methods such as std::make_unique or std::make_shared use. Both functions take a type T and arbitrary numbers of arguments args and forward them unchanged to the constructor of T. Have a look here:

template<typename T, typename... Args>                              // 1
std::unique_ptr<T> make_unique(Args&&... args)                      // 2
{
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));  // 3
}

 

This pattern is called perfect forwarding: If a function templates forward its arguments without changing their lvalue or rvalue characteristics, we call it perfect forwarding. 

Here is my previous post to perfect forwarding.

To get perfect forwarding for a function template, you have to follow the recipe consisting of three steps. It must not be a variadic template (...) such as for std::make_unique, so I skip this part.

  1. You need a template parameter: typename Args
  2. Take your function argument per forwarding reference: Args&& args
  3. Forward the function arguments: std::forward<Args>(args)

F.20: For “out” output values, prefer return values to output parameters

An explicit return value documents the intention of a function. Using a parameter with a reference as out output value may be misleading. This can also be an in-out value. Returning the result of a function by value holds also for the standard container that uses move semantic implicitly.

// OK: return pointers to elements with the value x
vector<const int*> find_all(const vector<int>&, int x);

// Bad: place pointers to elements with value x in-out
void find_all(const vector<int>&, vector<const int*>& out, int x);

 

There is an exception to this rule. If you have an expensive-to-move object, you can use a reference as out parameter.

struct Package {      // exceptional case: expensive-to-move object
    char header[16];
    char load[2024 - 16];
};

Package fill();       // Bad: large return value
void fill(Package&);  // OK

 

F.21: To return multiple “out” values, prefer returning a tuple or struct

Sometimes you function returns more than one out value. In this case, you should use a std::tuple or a struct but you should not use the parameter with a reference. This is very error-prone.

// BAD: output-only parameter documented in a comment
int f(const string& input, /*output only*/ string& output_data)
{
    // ...
    output_data = something();
    return status;
}

// GOOD: self-documenting
tuple<int, string> f(const string& input)
{
    // ...
    return make_tuple(status, something());
}

 

With C++17 and structured binding returning more than one value becomes quite convenient.

auto [value, success] = getValue(key);

if (success){
  // do something with the value;

 

The function getValue returns a pair. success indicates if the query for key was successful.

The next rule is special. For me, this rule is more a semantic rule. But anyway.

F.60: Prefer T* over T& when “no argument” is a valid option

If your parameter never can get a "no argument" such as a nullptr, you should use a T&T& cannot be a nullptr. If nullptr is possible, use T*.

std::string upperString(std::string* str){
  if (str == nullptr) return std::string{};  // check for nullptr
  else{
    ...
}

 

If no argument is an option, you have to check for it.

What's next

This post was about in, out, in-out, consume, and forward parameters but there are more questions to answer. How should you deal with sequences or with ownership? I will write about it in the next post.

 

 

Thanks a lot to my Patreon Supporter: Eric Pederson.

 

Get your e-book at leanpub:

The C++ Standard Library

Concurrency With Modern C++

Get Both as one Bundle

cover ConcurrencyCoverSmallFrame coverConcurrencyCoverSmallFrame
With C++11 and C++14 we got a lot of new C++ libraries. In addition, the existing ones are greatly improved. The key idea of my book is to give you the necessary information to the current C++ libraries in about 200 pages.

C++11 is the first C++ standard that deals with concurrency. The story goes on with C++17 and will continue with C++20.

I'll give you a detailed insight in the current and the upcoming concurrency in C++. This insight includes the theory and a lot of practice with more the 100 source files.

Get my books "The C++ Standard Library" and "Concurrency with Modern C++" in a bundle.

In sum, you get more than 500 pages full of modern C++ and more than 100 source files presenting concurrency in practice.

 

Comments   

0 #1 바카라사이트 2017-11-12 02:40
I am in fact thankful to the owner of this website who has shared this impressive post at here.
Quote

Add comment


My Newest E-Books

Latest comments

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 56

All 506522

Currently are 192 guests and no members online