Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Proper use of universal references

Before c++11, I used to write code like this:

// Small functions
void doThingsWithA(const A& a)
{
    // do stuff
}

void doThingsWithB(const B& b)
{
    // do stuff
}

void doThingsWithC(const C& c)
{
    // do stuff
}

// Big function
void doThingsWithABC(const A& a, const B& b, const C& c)
{
    // do stuff
    doThingsWithA(a);
    doThingsWithB(b);
    doThingsWithC(c);
    // do stuff
}

But now, with move semantics, it may become interesting (at least in some cases) to allow my functions to take rvalue references as parameters and add these overloads:

void doThingsWithA(A&& a);
void doThingsWithB(B&& b);
void doThingsWithC(C&& c);

From what I gather, if I want to be able to call those overloads within my big function, I need to use perfect forwarding, which may look like this (it is a bit less readable, but I guess that it can be ok with a good naming convention for the template types):

template<typename TplA, typename TplB, typename TplC>
void doThingsWithABC(TplA&& a, TplB&& b, TplC&& c)
{
    // do stuff
    doThingsWithA(std::forward<TplA>(a));
    doThingsWithB(std::forward<TplB>(b));
    doThingsWithC(std::forward<TplC>(c));
    // do stuff
}

My problem is this: doesn't that mean that if my small functions have other overloads, it will become possible to call the big one with parameters of types for which it was not intended?

I think that this may work to prevent this:

template<typename TplA, typename TplB, typename TplC,
class = typename std::enable_if<std::is_same<A, std::decay<TplA>::type>::value>::type,
class = typename std::enable_if<std::is_same<B, std::decay<TplB>::type>::value>::type,
class = typename std::enable_if<std::is_same<C, std::decay<TplC>::type>::value>::type>
    doThingsWithABC(TplA&& a, TplB&& b, TplC&& c)
{
    // do stuff
    doThingsWithA(std::forward<TplA>(a));
    doThingsWithB(std::forward<TplB>(b));
    doThingsWithC(std::forward<TplC>(c));
    // do stuff
}

Though I am not sure if it is not too restrictive, as I have no idea of how it behaves if I try to call the big functions with types that are implicitly convertible to A,B or C...

But... even supposing this works, do I really have no other options? (I mean... it's not easy on the eyes)

like image 232
Eternal Avatar asked Jun 19 '14 00:06

Eternal


People also ask

What is a universal reference?

Universal reference was a term Scott Meyers coined to describe the concept of taking an rvalue reference to a cv-unqualified template parameter, which can then be deduced as either a value or an lvalue reference.

What is reference collapsing?

Reference collapsing is the mechanism that leads to universal references (which are really just rvalue references in situations where reference-collapsing takes place) sometimes resolving to lvalue references and sometimes to rvalue references.

What are rvalue references?

Rvalue references is a small technical extension to the C++ language. Rvalue references allow programmers to avoid logically unnecessary copying and to provide perfect forwarding functions. They are primarily meant to aid in the design of higer performance and more robust libraries.

What is perfect forwarding?

Perfect forwarding allows a template function that accepts a set of arguments to forward these arguments to another function whilst retaining the lvalue or rvalue nature of the original function arguments.


2 Answers

Perfect forwarding is mainly for when you do not know how the data is going to be consumed, because you are writing a generic wrapper of 'user-supplied' data.

In a simple procedural system like you describe above, the 3 things you do will be concrete tasks.

That means you will know if they would benefit from having a movable source of data or not, and if they make sense if they have to copy, and if move is cheap.

If copy makes sense, but move is faster, and move is cheap (a common case), they should take parameters by value and move out of them when they store their local copy.

This rule then applies recursively to the function that calls the 3 sub functions.

If the function does not benefit from moving, take by const&.

If copy does not make sense, take by rvalue reference (not a universal reference) or by value.

In the case where it is both good to be able to move and move remains expensive should you consider perfect forwarding. As noted above, this usually only happens when wrapping functions set by the 'user' of your code base, as usually move is either really really cheap, or as expensive as copy. You have to be in an intermediate or indeterminate stage of move efficiency for perfect forwarding to be worthwhile.

There are other uses for perfect forwarding, such as container mutators, but they are more esoteric. As an example, my backwards range mutator will perfect forward the incoming range into storage in order to have reference lifetime extension work properly when you chain multiple range mutators in C++11 style ranged-based for(:) loops.

Madly perfect forwarding results in generated code bloat, slow builds, leaky implementations, and hard to understand code.

like image 87
Yakk - Adam Nevraumont Avatar answered Oct 30 '22 03:10

Yakk - Adam Nevraumont


Use static_asserts instead of enable_if. IMHO, this option is not only easier on the eyes, but also more user friendly. The compiler will print a clear error message if the argument types are violated, whereas with the enable_if counterpart it'll complain about no matching function being found.

template<typename TplA, typename TplB, typename TplC>
void doThingsWithABC(TplA&& a, TplB&& b, TplC&& c)
{
  static_assert(std::is_same<A, std::decay<TplA>::type>::value, "arg1 must be of type A");
  static_assert(std::is_same<B, std::decay<TplB>::type>::value, "arg2 must be of type B");
  static_assert(std::is_same<C, std::decay<TplC>::type>::value, "arg3 must be of type C");
    // do stuff
    doThingsWithA(std::forward<TplA>(a));
    doThingsWithB(std::forward<TplB>(b));
    doThingsWithC(std::forward<TplC>(c));
    // do stuff
}
like image 23
Praetorian Avatar answered Oct 30 '22 04:10

Praetorian