Search code examples
c++c++11parameter-passingrvalue-reference

c++11 optimal parameter passing


Consider these classes:

#include <iostream>
#include <string>

class A
{
    std::string test;
public:
    A (std::string t) : test(std::move(t)) {}
    A (const A & other) { *this = other; }
    A (A && other) { *this = std::move(other); }

    A & operator = (const A & other)
    {
        std::cerr<<"copying A"<<std::endl;
        test = other.test;
        return *this;
    }

    A & operator = (A && other)
    {
        std::cerr<<"move A"<<std::endl;
        test = other.test;
        return *this;
    }
};

class B
{
    A a;
public:   
    B (A && a) : a(std::move(a)) {}
    B (A const & a) : a(a) {}
};

When creating a B, I always have an optimal forward path for A, one move for rvalues or one copy for lvalues.

Is it possible to achieve the same result with one constructor? It's not a big problem in this case, but what about multiple parameters? I would need combinations of every possible occurrence of lvalues and rvalues in the parameter list.

This is not limited to constructors, but also applies to function parameters (e.g. setters).

Note: This question is strictly about class B; class A exists only to visualize how the copy/move calls gets executed.


Solution

  • The "by-value" approach is an option. It is not as optimal as what you have, but only requires one overload:

    class B
    {
        A a;
    public:   
        B (A _a) : a(move(_a)) {}
    };
    

    The cost is 1 extra move construction for both lvalues and xvalues, but this is still optimal for prvalues (1 move). An "xvalue" is an lvalue that has been cast to rvalue using std::move.

    You could also try a "perfect forwarding" solution:

    class B
    {
        A a;
    public:   
        template <class T,
                  class = typename std::enable_if
                  <
                     std::is_constructible<A, T>::value
                  >::type>
        B (T&& _a) : a(std::forward<T>(_a)) {}
    };
    

    This will get you back to the optimal number of copy/move constructions. But you should constrain the template constructor such that it is not overly generic. You might prefer to use is_convertible instead of is_constructible as I've done above. This is also a single constructor solution, but as you add parameters, your constraint gets increasingly complicated.

    Note: The reason the constraint is necessary above is because without, clients of B will get the wrong answer when they query std::is_constructible<B, their_type>::value. It will mistakenly answer true without a proper constraint on B.

    I would say that none of these solutions is always better than the others. There are engineering tradeoffs to be made here.