Perfect Forwarding (C++)

3 minute read

The code in this post is taken from this video. I just transcripted it here for the purpose of my easy accessing of the concepts explained in that video.

Example 1

In the code snippet below (explained in the post Move semantics) we saw that we have two constructor calls: a default constructor call and a move constructor call.

// perfect forwarding
#include <utility>
#include <vector>

struct Foo
{
  // a lot of member variables
}

int main()
{
  std::vector<Foo> v;
  {
    Foo f1; // default constructor call
    v.push_back(std::move(f1)); // move constructor call
  }
  return 0;
}

But we can just have one constructor call, like so:

// perfect forwarding
#include <utility>
#include <vector>

struct Foo
{
  Foo() = default;

  Foo(int i, bool b, float f)
    : m_i(i)
    , m_b(b)
    , m_f(f)
  {}

  int m_i;
  bool m_b;
  float m_f;
  // etc. (a lot more of member variables)

}

int main()
{
  std::vector<Foo> v;
  {
    v.emplace_back(1, true, 2.f); // will build foo directly in the vector space
  }
  return 0;
}

This will build the Foo object directly in the vector space by using perfect forwarding behind the scenes.

Example 2

Now say we add a new structure called Bar, which holds a private vector of Foo and a public AddFoo. We want to be able to give this AddFoo any type and any number of variables. Using variadic templates (template <typename ... Args>) we can have precisely that.

// perfect forwarding
#include <utility>
#include <vector>

struct Foo
{
  Foo() = default;

  Foo(int i)
    : m_i(i)
  {}

  Foo(int i, bool b, float f)
    : m_i(i)
    , m_b(b)
    , m_f(f)
  {}

  int m_i;
  bool m_b;
  float m_f;
  // etc. (a lot more of member variables)
}

struct Bar
{
  template <typename ... Args> // variadic templates
  void AddFoo(Args&& ... args) // && -> forwarding reference
  {
    v.emplace_back(forward<Args>(args)...);
    // forward -> keeps the value type of the argument 
    //            if it was lvalue, stays lvalue
    //            if it was rvalue, satys rvalue 
    // ... -> do it multiple times for each of the arguments (and their type)
  }
private:
  std::vector<Foo> v:
}

int main()
{
  Bar b;
  b.AddFoo(1, bool, 2.f);
  return 0;
}

Example 3

Imagine now we have a vector<int> as member variable in Foo, which we can initialize either by reference (thus copying the vector) or by move (thus stealing the vector resources). See comments in main().

// perfect forwarding
#include <utility>
#include <vector>

struct Foo
{
  Foo() = default;

  Foo(int i, bool b, float f, const std::vector<int> &v) // by reference
    : m_i(i)
    , m_b(b)
    , m_f(f)
    , m_v(v) // will copy the vector
  {}

  Foo(int i, bool b, float f, std::vector<int>&& v) // by move
    : m_i(i)
    , m_b(b)
    , m_f(f) // will steal the vector resources
  {}

  int m_i;
  bool m_b;
  float m_f;
  std::vector<int> m_v;
  // etc. (a lot more of member variables)
}

struct Bar
{
  template <typename ... Args> // variadic templates
  void AddFoo(Args&& ... args) // && -> forwarding reference
  {
    v.emplace_back(forward<Args>(args)...);
    // forward -> keeps the value types of the argument 
    //            if it was lvalue, stays lvalue
    //            if it was rvalue, satys rvalue 
    // ... -> do it multiple times for each of the arguments (and their type)
  }
private:
  std::vector<Foo> v:
}

int main()
{
  Bar b;
  std::vector<int> v = { 1, 2, 3 };

  // taking v by lvalue reference
  b.AddFoo(1, bool, 2.f, v); // v here is an lvalue
  // after this call, v will still be filled with its inital content

  // taking v by rvalue reference
  b.AddFoo(1, bool, 2.f, std::move(v)); // here we cast v to be an rvalue
  // after this call, v will be empty, because its resources have been stolen in the move constructor

  return 0;
}

Updated: