Jupiter
Jupiter

Reputation: 1522

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?

Upvotes: 1

Views: 51

Answers (2)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275740

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.

Upvotes: 2

Toby Speight
Toby Speight

Reputation: 30873

The usual approach is to pass by value, then move-construct the members from the parameters:

Foo(std::vector<int> a, std::vector<int> b)
    : a{ std::move(a) },
      b{ std::move(b) }
{}

If a copy is needed, it will be created by the caller and then moved-from to construct the member. If the caller passes a temporary (or other rvalue), no copy is made, only a single move.

For arguments that don't have an efficient move constructor, then accepting a reference to const is slightly more efficient, and I'd retain that.

None of this applies if the function doesn't need a copy of the passed value - continue to use a const ref if you don't modify the value and don't need it to live beyond the end of the function execution. Personally, I use pass-by-value-and-move liberally in my constructors, but rarely in my other functions.

Upvotes: 5

Related Questions