1

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?

Toby Speight
  • 27,591
  • 48
  • 66
  • 103
Jupiter
  • 1,421
  • 2
  • 12
  • 31

2 Answers2

5

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.

Toby Speight
  • 27,591
  • 48
  • 66
  • 103
  • Isn't it true that indeed temporaries will be moved but calling this ctor with a lvalue is slightly slower because it is copied and moved instead of just being copied? – Jupiter Aug 31 '17 at 15:22
  • 1
    Slightly, but the idea is that move-construction is expected to be very cheap. For a `std::vector`, you'll be hard pushed to measure the difference. And it's much less arduous and error-prone than writing 2^n different constructors! – Toby Speight Aug 31 '17 at 15:24
  • 1
    This approach is a good deal for `vector` because the move is so cheap compared to the copy. Just be careful about generalizing this approach to other types where you might not have this characteristic. – Howard Hinnant Aug 31 '17 at 15:33
  • @HowardHinnant Oke, so how would you deal with such cases? – Jupiter Aug 31 '17 at 15:37
  • This approach is generally OK for constructors but as OP says this question "also generalizes to all functions" be careful of functions like setters as it may be less efficient for lvalues because you don't reuse any existing capacity of member variables. – Chris Drew Aug 31 '17 at 15:49
  • @Chris, that's true - I use this idiom in constructors, but rarely elsewhere. – Toby Speight Aug 31 '17 at 15:52
  • 1
    @Jupiter: There's no one right way to do this. For each individual case, I count copies and moves, and guesstimate how expensive each is. There are 3 approaches, each with benefits and drawbacks. This one, the 2^n solution (which is great for single parameters), and the constrained template solution (perfect performance but ugly to read and write, easy to get wrong, and often overkill). For `vector`, I recommend the by-value approach. – Howard Hinnant Aug 31 '17 at 15:57
  • @HowardHinnant Could you give me a link to a source explaining the constraint template solution? I'm not familiar with it. Now on a general note, the thing that bothers me about all of this is that for every single case there is a single clear thing to do. Elide copy/move where possible, other wise move and copy the arguments as a last resort. All this info is available at compile-time (at the call site at least) but it is impossible to create simple and easy to read and write code that does this. I wonder why the c++ committee has designed the system like this. – Jupiter Aug 31 '17 at 16:22
  • 2
    Renovating an older house is often more difficult and expensive than building a new one from scratch. It takes a different skill set, and the result will not be the same. But sometimes renovation is the only practical answer, especially if you can't throw people out of the house while the work is being done. – Howard Hinnant Aug 31 '17 at 16:28
2

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.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524