Type deduction of ternary operator with mixed lvalue and rvalue usage

249 Views Asked by At

Recently, I came across own code that I wrote accidently this way (much simplified here):

#include <iostream>
#include <string>

void foo(std::string&& value)
{
  std::cout << value << std::endl;
  // for instance modify value here somehow optionally and move it further on ...
  // foo2(std::move(value));
}

int main()
{
  std::string value = "Hello world";

  for (size_t idx = 0; idx < 2; ++idx)
  {
    foo(idx < 1 ? value : std::move(value));
  }
}

Please no comments about the questionable scheme at all! This happened during a long night session... :)

I just want to know, what's actually going on here in terms of standard behavior. Type deduction of the ternary operator leads to an rvalue obviously, but I'd like to know the exact fitting phrase(s) within the standard since the observable effect is such that for the idx < 1 case, an rvalue to a copy(?) of value is produced.

I read https://timsong-cpp.github.io/cppwp/n4659/expr.cond but that doesn't seem to be enough to explain the behavior here since I expected a direct rvalue conversion (originally, I expected a compiler error though). I also thought there might be some duplicates here on SO about this but I wasn't able to find the really fitting one(s) here so far. So sorry in advance if there are quite fast detectable ones however!

2

There are 2 best solutions below

3
On BEST ANSWER

According to cppinsights.io the compiler is creating a temporary std::string.

foo((idx < 1 ? std::basic_string<char, std::char_traits<char>, std::allocator<char> >(value) : std::basic_string<char, std::char_traits<char>, std::allocator<char> >(std::move(value))));

And here the version that separates the ternary operator from the function call:

auto&& x = idx < 1 ? value : std::move(value);
foo(std::move(x));

cppinsights.io says

std::basic_string<char, std::char_traits<char>, std::allocator<char> > && x = (idx < 1 ? std::basic_string<char, std::char_traits<char>, std::allocator<char> >(value) : std::basic_string<char, std::char_traits<char>, std::allocator<char> >(std::move(value)));
foo(std::move(x));

I am trying to understand that reading the standard n4835.pdf, I hope this is correct:

7.6.16 Conditional operator [expr.cond]
4 Otherwise, if the second and third operand have different types and either has (possibly cv-qualified) class type, or if both are glvalues of the same value category and the same type except for cv-qualification, an attempt is made to form an implicit conversion sequence (12.4.3.1) from each of those operands to the type of the other. [...] Attempts are made to form an implicit conversion sequence from an operand expression E1 of type T1 to a target type related to the type T2 of the operand expression E2 as follows:
(4.1) — If E2 is an lvalue, the target type is “lvalue reference to T2”, subject to the constraint that in the conversion the reference must bind directly (9.4.3) to a glvalue.
(4.2) — If E2 is an xvalue, the target type is “rvalue reference to T2”, subject to the constraint that the reference must bind directly.
(4.3) — If E2 is a prvalue or if neither of the conversion sequences above can be formed and at least one of the operands has (possibly cv-qualified) class type:
[...] (4.3.3) — otherwise, the target type is the type that E2 would have after applying the lvalue-to-rvalue (7.3.1),

4.1 and 4.2 do not apply, so we are coming to 4.3.3. and there to 7.3.1 where 3.2 applies.

7.3.1 Lvalue-to-rvalue conversion [conv.lval] 3 The result of the conversion is determined according to the following rules:
[...] (3.2) — Otherwise, if T has a class type, the conversion copy-initializes the result object from the glvalue.

Please note that 7.6.16 4 says that conversions are tried in both directions, thus don't focus too much on the "E2" in 4.3(.3).

2
On

I see it this way:

idx < 1 ? value : std::move(value) 

is an expression.

The result of evaluating this expression is an rvalue always.

In the case of idx < 1: the rvalue evaluated by this expression is contructed by copy-constructing from value.

In the case of idx >=: the rvalue evaluated by this expression is constructed by move-constructing from value.

So at the end, the function argument value in void foo(std::string&& value) is binding always to an rvalue by evaluating this expression.

It's only the difference in the way how this expression value itself is being constructed: in the first case by copy-constructing, in the second case by move-constructing.