Proper use of universal references

2019-04-29 07:27发布

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)

2条回答
三岁会撩人
2楼-- · 2019-04-29 07:33

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.

查看更多
叼着烟拽天下
3楼-- · 2019-04-29 07:58

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
}
查看更多
登录 后发表回答