13

In the following scenario

template <class T>
? f(T&& a, T&& b)
{
    return a > b ? a : b; 
}

what would the optimum return type be ? My thoughts so far are :

  1. Return by r value refs, perfectly forwarding the function arguments :

    template <class T>
    decltype(auto) f(T&& a, T&& b)
    {
        return a > b ? forward<T>(a) : forward<T>(b); 
    }
    
  2. Move construct the return value :

    template <class T>
    auto f(T&& a, T&& b)
    {
        return a > b ? forward<T>(a) : forward<T>(b); 
    }
    
  3. Try fo find a way to enable (N)RVOs (even though I think since I want to use the function arguments that's impossible)

Is there a problem with these solutions ? Is there a better one ? What's the best practice ?

Lorah Attkins
  • 5,331
  • 3
  • 29
  • 63
  • 2
    How do you expect to have a return type that can return two different types? T or U? – xaxxon Mar 17 '16 at 07:09
  • 1
    @xaxxon Say T is `int` and `U` is `double`. What will the type of the ternary operator be? I'll let you pick it up from there, you get the gist – Lorah Attkins Mar 17 '16 at 07:49
  • @xaxxon but since the question is not about that, I'll edit to avoid confusions – Lorah Attkins Mar 17 '16 at 07:51
  • 1
    @xaxxon As long as `T` can be converted to `U` (or the inverse, or some more complicated stuff as defined per the standard), this is perfectly valid. – Holt Mar 17 '16 at 07:52
  • but there is no sfinae to enforce them being convertible types, though (question since edited) – xaxxon Mar 17 '16 at 07:57
  • Why not `auto f(T&& a, T&& b) -> decltype(a+b)` ? – Ajay Mar 17 '16 at 07:57
  • the types might not even be convertible to that type. – xaxxon Mar 17 '16 at 07:58
  • You haven't even said what you want the behavior to be, have you @LorahAttkins? Do you want to return a pointer? a copy? a reference? – xaxxon Mar 17 '16 at 08:01
  • 1
    @Ajay because of things like `char a,b; decltype(a+b) c; std::cout << sizeof(c);` printing 4 – PeterT Mar 17 '16 at 08:05
  • 1
    @xaxxon Oh I see. I want to return a forwarding reference parameter (I'm sorry I can't elaborate on what that is, there are plenty of tutorials online). What that means is that if `T` is a reference I want to return a reference and if `T` is an rvalue I want to either forward that rvalue through the function or move construct a value (that will become an xvalue in the expression where it's used). I'm I getting through? – Lorah Attkins Mar 17 '16 at 08:07
  • @xaxxon But the actual [value category](http://en.cppreference.com/w/cpp/language/value_category) is in question so that's why I ask for a "best practice" (I mean not for the case of lvalue params, there I'll have to have a reference obviously) – Lorah Attkins Mar 17 '16 at 08:09
  • To begin with, using `T` twice, as a forwarding reference, limits the scope of usability of your function – Piotr Skotnicki Mar 17 '16 at 08:37
  • 4
    `template decltype(auto) f(T&& a, U&& b)` is fine; alternatively, you can make it sfinae-able, `template auto f(T&& a, U&& b) -> decltype(a > b ? forward(a) : forward(b))`. in both cases, the compiler (following the rules for determining a common type of two operands of the conditional operator), will figure out the appropriate return type (if there's such). I don't think you can gain anything more than that – Piotr Skotnicki Mar 17 '16 at 08:38
  • 1
    Next time, please try to be precise with your question, so it doesn't turn into such a clown fiesta. A lot of people's time was wasted and it's still not clear what question is being answered by what answer – xaxxon Mar 17 '16 at 22:04
  • sfinae can enforce that the function doesn't get called unless the types are convertible. you can't return "a forwarding reference". Also with your broken english, maybe you don't understand what you actually said as much as you think you do. Being abusive on SO just gets your comments deleted, so that's not helping, either. – xaxxon Mar 20 '16 at 22:48
  • @xaxxon "sfinae can enforce that the function doesn't get called unless the types are convertible" How will it go about doing this ? Or to put it differently, how will your idea of such an "enforcement" differ from a compilation error caused by the types not being convertible (to one another may I add) – Lorah Attkins Mar 20 '16 at 23:00
  • @xaxxon ''' you can't return "a forwarding reference" ''' that's literally what `std::forward` is doing – Lorah Attkins Mar 20 '16 at 23:18
  • @LorahAttkins a "forwarding reference" is not a type. And if you literally wanted to "return the type returned by std::forward", then what is your actual question? You said "what would the optimum return type be ?" but you're saying you want the result of std::forward, so the question doesn't make sense if you're stating the answer was present in the question. You should consider just deleting this question as it's not providing any value to anyone in its current state. – xaxxon Mar 21 '16 at 01:04
  • @xaxxon It's not a type, never said it is. No I don't "want" to return the type returned by std::forward, I used std::forward as an example. My question is "If I have a forwarding reference parameter what's the optimum way to return it?". I'd never ask in the first place if I knew of the hostility I'd come across and you should consider deleting your comments because they don't compliment your knowledge of c++. Now, I won't answer to you anymore unless it's a technical matter. – Lorah Attkins Mar 21 '16 at 07:18
  • @LorahAttkins You asked "what would the optimum return type be" If you want "the result of std::forward" then the return type is fixed. There is no question as to what the return type is. If you didn't use the right words, you should update the question so it actually says what you intended. – xaxxon Mar 21 '16 at 07:29

2 Answers2

7

For option 1, you need to use two template parameters to enable forwarding to happen when the two arguments have different value categories. It should be

template <typename T, typename U>
decltype(auto) f(T&& a, U&& b)
{
    return a > b ? std::forward<T>(a) : std::forward<U>(b);
}

What actually gets returned here is pretty tricky to work out. First you need to know the value categories of a and b, and what T and U get deduced as. Then whatever pairing you choose goes through the very complicated rules for the ternary operator. Finally, the output of that goes through the decltype rules to give you the actual return type of the function.

I did actually sit down and work it all out once, with a little help from SO. To the best of my recollection, it goes like this: the result will be a mutable lvalue reference if and only if a and b are lvalue references of compatible types; the result will be a const lvalue reference if one of a and b is a const lvalue reference and the other is any sort of reference; otherwise f will return a new value.

In other words, it does the right thing for any given pair of arguments -- probably because somebody sat down and wrote the rules exactly so that it would do the right thing in all cases.

Option 2 is doing to do exactly the same as option 1, except that the decltype rules don't come in to play, and the function will always return a new value -- plain auto never deduces to a reference.

I can't think of an option 3 that would work RVO-wise, since as you say you're using the arguments.

All in all, I think (1) is the answer you're looking for.

Community
  • 1
  • 1
Tristan Brindle
  • 16,281
  • 4
  • 39
  • 82
5

It depends on your intentions and the move awareness of your T. Each case is valid but there are differences in the behavior; this :

template <class T>
decltype(auto) f(T&& a, T&& b)
{
    return a > b ? forward<T>(a) : forward<T>(b); 
}

will perfectly forward an lvalue or rvalue reference resulting in no temporaries at all. If you put an inline there (or the compiler puts it for you) is like having the parameter in place. Against "first sight intuition" it won't produce any runtime error due to 'referencing' a dead stack object (for the rvalue case), since any temporaries we forward come from a caller above us (or below us, depends on how you think of stacks during function calls). On the other hand, this (in the rvalue case):

template <class T>
auto f(T&& a, T&& b)
{
    return a > b ? forward<T>(a) : forward<T>(b); 
}

will move-construct a value (auto will never deduce a & or &&) and if T is not move constructible, then you'll invoke a copy constructor (a copy will always be made if T is an lvalue reference). If you use f in an expression you'll produce an xvalue after all (and that makes me wonder if the compiler can use the initial rvalue, but I wouldn't bet on it).

Long story short, if the code that uses f is build in a way that handles both rvalue and lvalue references, I'd go with the first choice, after all that's the logic std::forward is built around plus you won't produce any copies when you have lvalue references.

Nikos Athanasiou
  • 29,616
  • 15
  • 87
  • 153
  • @PiotrSkotnicki To my knowledge the initial question was edited to remove the `U` to avoid confusion, step away from SFINAE and type promotion matters and focus on the actual topic which was the proper use of perfect forwarding, move semantics and rvalue refs. Does this answer your q? (the other answer quotes code from the original version of the question) – Nikos Athanasiou Mar 17 '16 at 10:06
  • @PiotrSkotnicki Or do you mean something like `return std::forward(a > b ? a : b);` ie rearrangement of expression to avoid repeating type info? – Nikos Athanasiou Mar 17 '16 at 10:08
  • 1
    No, I mean that `T` used twice is likely to cause a deduction failure in any case when the value category (and type) differs across argument expressions – Piotr Skotnicki Mar 17 '16 at 10:09
  • @PiotrSkotnicki Yes, but that's the user's problem. I'm assuming `a` and `b` are of the same value category (as if there was a `static_assert` in there) to answer the question "how to return a forwarding reference parameter". If we were to provide for all combinations ( {Lv, Lv}, {Lv, Rv}, {Rv, Lv}, {Rv, Rv} ) we'd have to put some more template machinery the OP says nothing about – Nikos Athanasiou Mar 17 '16 at 10:16
  • @PiotrSkotnicki Also if a `T` and a `U` were present how would we return a reference to either of them ? (in the case they were lvalues of different types). Enforcing the same T (plus deduction enforcing the same value category or fail in compilation) makes the resulting return type a typeswitch solely on value category – Nikos Athanasiou Mar 17 '16 at 10:46
  • *"Also if a T and a U were present how would we return a reference to either of them ?"*, e.g., if `a` is of a base class type, and `b` is of a derived class type, and both are lvalues or both are rvalues, then the result is a (l-/r-value) reference to base class type. A deduction failure is not the best way of forcing equal types. And your answer certainly isn't "the best practice" OP seeks. Even if it's an XY problem. – Piotr Skotnicki Mar 17 '16 at 11:13
  • @PiotrSkotnicki Nooo, nope Piotr, don't circumvent this, I said "different", convertible to one another doesn't quite capture the essence of different. Plus I answered your points, I don't see you giving any credit , you just try to find a way to make your previous points stand. If an OP has an example to ask a question I don't get consumed in the example, I try to answer the question. I'll cherish nitpicks from people who know what they're talking about (that's why I'm responding to you) but won't get bullied into nitpicking the meat out of an answer. – Nikos Athanasiou Mar 17 '16 at 11:14
  • @PiotrSkotnicki Also, since it looks like we agree on what's the question about, if there's a different best practice I'd urge you to answer (and I promise my +1), your contribution is appreciated. – Nikos Athanasiou Mar 17 '16 at 11:23
  • *"I said different, convertible to one another doesn't quite capture the essence of different"*, then you mean *disparate* types that have nothing in common whatsoever? And you claim that it's better to completely constrain the allowed set of arguments (to those having the same cv-qualification, the same value category, the same type), just because there are non-sensible cases when it won't work. – Piotr Skotnicki Mar 17 '16 at 11:47
  • 1
    @PiotrSkotnicki Nikos is correct in his assumptions. I couldn't care less about having 2, 3 or 4 types, I'm merely asking how get the value out of the function, should I forward it or move construct the return value? Maybe I should edit to only have one parameter ? (apart from that, after the edit, the signature of `f` is as given, I don't need an answer to change that, I'm assuming this answer goes like "well if you have that signature, then... ) – Lorah Attkins Mar 17 '16 at 20:35
  • 1
    @LorahAttkins it's not only a matter of equal types, but also equal value categories. If you want to avoid confusion, then using a single parameter function would be a better example – Piotr Skotnicki Mar 17 '16 at 21:00
  • 1
    I agree with @PiotrSkotnicki 's point. I think this answer is not very useful, because simple code like [this](http://coliru.stacked-crooked.com/a/53e99ba61529e591) fails to compile. – ildjarn Mar 20 '16 at 23:50