How to define a function to work with move semantics and copy semantics?
Asked Answered
D

2

5

Suppose that I am implementing a collection and I want to add an element to it, something like.

template <typename T>
class MyCollection
{
    void add(const T& element);
};

Now, since adding element usually means copying it, for efficiency reason it makes sense to have the following version of add as well void add(T&& element). Now the problem is that, obviously the code for both functions is exactly the same, with only difference being the argument type. My command of C++ is limited at the moment, but I would like to know whether there is a simple and idiomatic way to write the addthe function once without rewriting it twice?

Dollarfish answered 27/4, 2020 at 20:21 Comment(0)
B
6

In fact this is solved by defining a single overload:

void add(T element) {
    where_it_is_actually_stored.insert(std::move(element));
}

Next, depending on whether you're adding a lvalue, a rvalue made from a moved lvalue, or a temporary object, the compiler will resolve an appropriate constructor so your value argument would be either copied or moved.

Byrdie answered 27/4, 2020 at 20:26 Comment(6)
I might be missing something, but doesn't this code just copy the value of T(which I want to avoid in first place) and then just casts element into rvalue? I was living (may be under wrong impression) that std::move<T>(x) was sort of equivalent to static_cast<T &&>(x)? Was I wrong?Dollarfish
@RazielMagius: If the type T is move-constructible, and add is called on an rvalue argument, then this won't copy that argument; it will move it.Mystique
@RazielMagius Well, assuming that T supports move semantics, you'd move the element into the parameter, if passed as rvalue reference, otherwise you'd copy it into. However you get one additional move in both cases (unless compiler is able to optimise away). That's the price for defining just one single function...Hypersensitize
However, if T doesn't support move semantics, you indeed get two copies! If you are lucky, though, compiler optimises one of these away.Hypersensitize
@Hypersensitize Let's not forget that there are three basic value categories. For a prvalue, both versions perform one move. For an xvalue, the overloaded version moves once (binding the xvalue to a T&&), while this moves twice. For an lvalue, a T& overload would perform a copy. This version copies then moves. If T supports move semantics, then moves are cheap and it don't really matter. If it doesn't, moves become copies, and that's very unfortunate.Sovereign
This seems to be the and almost optimal answer. The answer provided by @Sovereign seems to be the optimal one (after consulting the literature...). Thanks for all the clarifications and answers.Dollarfish
S
4

The most general solution, I think, would actually be this. This is what the standard library provides, so I suppose that makes it "idiomatic".

template<typename T>
struct my_collection {
    template<typename... Args>
    void emplace(Args&&... args) {
        // construct the T object directly in its place out of std::forward<Args>(args)...
    }
};
Sovereign answered 27/4, 2020 at 21:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.