7
\$\begingroup\$

Perfect forwarding is usually done with std::forward. E.g.,

template<typename T>
void foo( T&& t ) {
    bar(std::forward<T>(t));
}

for some function bar(). Sometimes, only a member of t, say t.x, needs to be forwarded. This is also a solved problem

template<typename T>
void foo( T&& t ) {
    bar(std::forward<T>(t).x);
}

...But what if t has a range member, say t.range, and bar takes an element from said range? E.g.,

template<typename T>
void foo( T&& t ) {
    for (auto& r : t.range)
        bar(???(r));
}

Here, ??? is a placeholder for a cast which forwards the type of r as the type of t itself would be forwarded.

Note that simply writing

template<typename T>
void foo( T&& t ) {
    for (auto&& r : std::forward<T>(t).range)
        bar(r);
}

won't work as r is still named and will always pass by value or l-value reference but never by r-value reference. To solve this I introduce forward_as<T>

////////////////////////////////////////////////////////////////////////////////
/// Forward As
/// 
/// Forwards u of type U as T would be forwarded. I.e., u is forwarded
/// like some t would be forwarded by std::forward<T>(t). The cv-qualifiers of 
/// U are preserved. E.g., forward_as<T, std::string>(u) casts u to:
///
///  (1) std::string& if T is int&
///  (2) std::string& if T is const int&
///  (3) std::string&& if T is int&&
///
/// Analogously, forward_as<T, const std::string>(u) casts u to:
///
///  (1) const std::string& if T is int&
///  (2) const std::string& if T is const int&
///  (3) const std::string&& if T is int&&
/// 
////////////////////////////////////////////////////////////////////////////////
template<typename T, typename U>
auto forward_as( U&& u ) -> std::conditional_t<
        std::is_reference<T>::value, 
        std::remove_reference_t<U>&, 
        std::remove_reference_t<U>&&>
{
    return static_cast<std::conditional_t<
        std::is_reference<T>::value, 
        std::remove_reference_t<U>&, 
        std::remove_reference_t<U>&&>>(u);
}

Note that I use auto with a trailing return type. This is because my compiler is old. It should work just fine with decltype(auto) on a C++14 compliant compiler. Also note that I've chosen to preserve cv-qualifiers of the given type.

Now the problem from before can be solved by

template<typename T>
void foo( T&& t ) {
    for (auto& r : t.range)
        bar(forward_as<T>(r));
}

Direct member forwarding is also possible using forward_as<T>(t.x).

Have I missed any edge cases? Is there an alternative to all of this? Any suggestions for interface/naming/style improvements?

Edit: I've made a code sample. The syntax is slightly more verbose since the ideone compiler only supports C++11. The underlying principle is the same.

\$\endgroup\$
4
\$\begingroup\$

I'll start with some direct review of what you've implemented. After the bullets, however, I go into some concerns about whether this should be used. Thanks for the interesting idea; reviewing it made me re-examine several things I thought I already knew.

  • Like std::forward you should consider making forward_as a constexpr function.
  • You could avoid some of the pre-C++14 verbosity by using decltype inside forward_as (although the auto return type sounds better still):

        return static_cast<decltype(forward_as<T, U>(u))>(u);
    
  • How do you know whether for (auto&& r : t.range) will iterate over objects owned by t?

  • Admittedly a bikeshed point, I don't like the way the name forward_as<T>(u) reads; perhaps forward_if<T>(u) or even move_if_rvalue<T>(u) would help this not look like it returns an expression of a T-based type.

However like Kerrek SB's comment on the referenced question, I worry about the state of the main object when forwarding or moving its members. What are the rules for doing this safely? Your examples seem to show forwarding only a single member, but what are the rules when forwarding different parts of the same object? What are the code review guidelines to ensure that problems are not introduced in later edits?

To sum up, I mistrust this. It might just be my own lack of familiarity with such a use case. But it also reflects an uncertainty that optimizing for move is likely to be worth it in these cases. If this is actually a bottleneck or potential bottleneck in the code, are there better alternatives? Does bar even overload on r-value types? Should foo receive t.range instead of t, or will std::for_each be able to generate r-values automatically:

    std::for_each(std::begin(t.range), std::end(t.range), bar)

(If so, I haven't figured out the syntax to support it in your sample with bar as a function template. Using instead a struct with overloaded operator(), it appears to suggest we shouldn't assume the range contains r-values.)

\$\endgroup\$
  • \$\begingroup\$ Thanks for reviewing! Good point with constexpr. I didn't even know of that pre-C++14 decltype trick. I'll definitely add that. Your 3rd point I don't get, sorry. Can you provide an example? I like your name suggestions. Especially move_if_rvalue. That one is much clearer. -- Yes, it is definitely a rare situation to only move out t.x and even rarer to move t.range elementwise. Still, I've had good reason to do both in a practical setting. Performance-wise it mattered because the r elements contained std::vectors... And yes, my bar was overloaded for r-value references. [1/2] \$\endgroup\$ – Frederik Aalund Nov 2 '14 at 15:25
  • \$\begingroup\$ After a move, the moved-from element is still in a valid but unspecified state. So even though we have moved stuff out of t we can still expect t itself to be in a valid but unspecified state. That is, we can expect t to destruct gracefully even though we have moved some of t's public members. -- I like your algo_foo suggestion but I do think that we should move the r's if we know that t is an r-value reference. Why not use that knowledge? Again, what a nice and detailed answer. Cheers! [2/2] \$\endgroup\$ – Frederik Aalund Nov 2 '14 at 15:38
  • \$\begingroup\$ @FrederikAalund: Regarding the third bullet, my intent was to consider a proxy object, or an object reflecting shared ownership (such as a shared_ptr). In such cases, even if t was moved, the items in the range are owned elsewhere and thus can't always be moved. (I gather this isn't the case in your code, so this may be just a straw man argument, but it gives me pause.) \$\endgroup\$ – Michael Urman Nov 3 '14 at 3:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.