Search code examples
c++11move-semanticsrvalue-reference

Idiomatic way of providing constructors that move their arguments


Lets say I have the following class:

#include <vector>
class Foo
{
public:
    Foo(const std::vector<int> & a, const std::vector<int> & b)
        : a{ a }, b{ b } {}
private:
    std::vector<int> a, b;
};

But now I want to account for the situations in which the caller of the constructor might pass temporaries to it and I want to properly move those temporaries to a and b.

Now do I really have to add 3 more constructors, 1 of which has a as a rvalue reference, 1 of which has b as a rvalue reference and 1 that only has rvalue reference arguments?

Of course this question generalizes to any number of arguments which are worthwhile to move and the number of required constructors would be arguments^2 2^arguments.

This question also generalizes to all functions.

What is the idiomatic way of doing this? Or am I completely missing something important here?


Solution

  • Really, you should take by value if move construction is very cheap.

    This results in exactly 1 extra move over the ideal case in every case.

    But if you really must avoid that, you can do this:

    template<class T>
    struct sink_of {
      void const* ptr = 0;
      T(*fn)(void const*) = 0;
      sink_of(T&& t):
        ptr( std::addressof(t) ),
        fn([](void const*ptr)->T{
          return std::move(*(T*)(ptr));
        })
      {}
      sink_of(T const& t):
        ptr( std::addressof(t) ),
        fn([](void const*ptr)->T{
          return *(T*)(ptr);
        })
      {}
      operator T() const&& {
        return fn(ptr);
      }
    };
    

    which uses RVO/elision to avoid that extra move at the cost of a bunch of pointer-based overhead and type erasure.

    Here is some test code that demonstrates that

    test( noisy nin ):n(std::move(nin)) {}
    test( sink_of<noisy> nin ):n(std::move(nin)) {}
    

    differ by exactly 1 move-construct of a noisy.

    The "perfect" version

    test( noisy const& nin ):n(nin) {}
    test( noisy && nin ):n(std::move(nin)) {}
    

    or

    template<class Noisy, std::enable_if_t<std::is_same<noisy, std::decay_t<Noisy>>{}, int> = 0 >
    test( Noisy && nin ):n(std::forward<Noisy>(nin)) {}
    

    has the same number of copy/moves as the sink_of version.

    (noisy is a type that prints information about what moves/copies it engages in, so you can see what gets optimized away by elision)

    This is only worth it when the extra move is important to eliminate. For a vector it is not.

    Also, if you have a "true temporary" you are passing, the by-value one is as good as the sink_of or "perfect" ones.