2

Recently I find myself often in the situation of having a single function that takes some object as a parameter. The function will have to copy that object.

However the parameter for that function may also quite frequently be a temporary and thus I want to also provide an overload of that function that takes an rvalue reference instead a const reference.

Both overloads tend to only differ in that they have different types of references as argument types. Other than that they are functionally equivalent.

For instance consider this toy example:

void foo(const MyObject &obj) {
    globalVec.push_back(obj); // Makes copy
}
void foo(MyObject &&obj) {
    globalVec.push_back(std::move(obj)); // Moves
}

Now I was wondering whether there is a way to avoid this code-duplication by e.g. implementing one function in terms of the other.

For instance I was thinking of implementing the copy-version in terms of the move-one like this:

void foo(const MyObject &obj) {
    MyObj copy = obj;
    foo(std::move(copy));
}
void foo(MyObject &&obj) {
    globalVec.push_back(std::move(obj)); // Moves
}

However this still does not seem ideal since now there is a copy AND a move operation happening when calling the const ref overload instead of a single copy operation that was required before.

Furthermore, if the object does not provide a move-constructor, then this would effectively copy the object twice (afaik) which defeats the whole purpose of providing these overloads in the first place (avoiding copies where possible).

I'm sure one could hack something together using macros and the preprocessor but I would very much like to avoid involving the preprocessor in this (for readability purposes).

Therefore my question reads: Is there a possibility to achieve what I want (effectively only implementing the functionality once and then implement the second overload in terms of the first one)?

If possible I would like to avoid using templates instead.

Enlico
  • 23,259
  • 6
  • 48
  • 102
Raven
  • 2,951
  • 2
  • 26
  • 42

2 Answers2

5

My opinion is that understanding (truly) how std::move and std::forward work, together with what their similarities and their differences are is the key point to solve your doubts, so I suggest that you read my answer to What's the difference between std::move and std::forward, where I give a very good explanation of the two.


In

void foo(MyObject &&obj) {
    globalVec.push_back(obj); // Moves (no, it doesn't!)
}

there's no move. obj is the name of a variable, and the overload of push_back which will be called is not the one which will steal reasources out of its argument.

You would have to write

void foo(MyObject&& obj) {
    globalVec.push_back(std::move(obj)); // Moves 
}

if you want to make the move possible, because std::move(obj) says look, I know this obj here is a local variable, but I guarantee you that I don't need it later, so you can treat it as a temporary: steal its guts if you need.

As regards the code duplication you see in

void foo(const MyObject &obj) {
    globalVec.push_back(obj); // Makes copy
}
void foo(MyObject&& /*rvalue reference -> std::move it */ obj) {
    globalVec.push_back(std::move(obj)); // Moves (corrected)
}

what allows you to avoid it is std::forward, which you would use like this:

template<typename T>
void foo(T&& /* universal/forwarding reference -> std::forward it */ obj) {
    globalVec.push_back(std::forward<T>(obj)); // moves conditionally
}

As regards the error messages of templates, be aware that there are ways to make things easier. for instance, you could use static_asserts at the beginning of the function to enfornce that T is a specific type. That would certainly make the errors more understandable. For instance:

#include <type_traits>
#include <vector>
std::vector<int> globalVec{1,2,3};

template<typename T>
void foo(T&& obj) {
    static_assert(std::is_same_v<int, std::decay_t<T>>,
                  "\n\n*****\nNot an int, aaarg\n*****\n\n");
    globalVec.push_back(std::forward<T>(obj));
}

int main() {
    int x;
    foo(x);
    foo(3);
    foo('c'); // errors at compile time with nice message
}

Then there's SFINAE, which is harder and I guess beyond the scope of this question and answer.

My suggestion

Don't be scared of templates and SFINAE! They do pay off :)

There's a beautiful library that leverages template metaprogramming and SFINAE heavily and successfully, but this is really off-topic :D

Enlico
  • 23,259
  • 6
  • 48
  • 102
  • Thanks for the answer. This would then require turning all my functions into templates which is something that I don't like at all due to the requirement of having to implement them all in header files and also due to the horrible error messages that these produce... But since it answers my question I upvoted nonetheless (I am still hoping for other options though) – Raven Jul 07 '21 at 08:18
  • `globalVec.push_back(obj); // Moves` -> `// No Move` ? – 463035818_is_not_an_ai Jul 07 '21 at 08:23
  • 1
    @Raven You don't have to implement them in header files if you only use finite set of template instantiations. – eerorika Jul 07 '21 at 08:24
  • template has also the "drawback" (or it is the `{/*..*/}` syntax issue) to not allow to pas `{/*..*/}` (as `{/*..*/}` is not deducible in that context). – Jarod42 Jul 07 '21 at 08:24
  • 1
    @463035818_is_not_a_number, it's the original comment. I'll change it. – Enlico Jul 07 '21 at 08:24
  • 1
    comments are liars, I know why I dont like them :P – 463035818_is_not_an_ai Jul 07 '21 at 08:28
  • @eerorika How would I do this syntactically? Declare a template function and then the specific specializations in the header while implementing the specific specializations in a CPP file? – Raven Jul 07 '21 at 08:28
  • @Raven Declare the template in a header, define it in a source file along with explicit instantiations that you're going to use. – eerorika Jul 07 '21 at 08:30
  • @eerorika Ah I see. Thanks. However if I understand this correctly it seems that this would cause a misuse of the function (with a type it was not specialized for) to produce a linker error instead of a compiler error, right? Is there a way to avoid this? EDIT: I guess SFINAE would be the way to go for that, right? – Raven Jul 07 '21 at 08:35
  • 2
    @Raven You could define a wrapper template function that you do define in the header, which only `static_assert`s that the template argument is correct, and then delegates to the non-inline template. But at this point you're going in the wrong direction if your goal is to reduce boilerplate. (or yeah SFINAE, but that's also horrible) – eerorika Jul 07 '21 at 08:36
3

A simple solution is:

void foo(MyObject obj) {
    globalVec.push_back(std::move(obj));
}

If caller passes an lvalue, then there is a copy (into the parameter) and a move (into the vector). If caller passes an rvalue, then there are two moves (one into parameter and another into vector). This can potentially be slightly less optimal compared to the two overloads because of the extra move (slightly compensated by the lack of indirection) but in cases where moves are cheap, this is often a decent compromise.

Another solution for templates is std::forward explored in depth in Enlico's answer.

If you cannot have a template and the potential cost of a move is too expensive, then you just have to be satisfied with some extra boilerplate of having two overloads.

eerorika
  • 232,697
  • 12
  • 197
  • 326