I was writing some code similar to:
std::string foo(bool b, const std::string& fst, std::string&& snd) {
return b ? fst : std::move(snd);
}
and clang and copied snd
out while gcc
moved it out.
I tried to minimize the example and I came up with:
#include <iostream>
#include <utility>
struct printer {
printer() { }
printer(const printer&) { std::cout << "copy" << std::endl; }
printer(printer&&) { std::cout << "move" << std::endl; }
printer(const printer&&) { std::cout << "const rvalue ref" << std::endl; }
};
int main() {
const printer fst;
printer snd;
false ? fst : std::move(snd);
}
gcc 5.2 outputs
move
clang 3.6 outputs
const rvalue ref
Does the standard allow both the gcc and clang behavior?
Random observations below:
Both gcc and clang unify the type of the ternary to:
const printer
gcc 5.2 disassembly
clang 3.6 disassembly
The ?: ternary operator produces a value, and all three of its operands are expressions. break is a statement, not an expression. It does not produce a value.
The conditional (ternary) operator is the only JavaScript operator that takes three operands: a condition followed by a question mark ( ? ), then an expression to execute if the condition is truthy followed by a colon ( : ), and finally the expression to execute if the condition is falsy.
The ternary operator take three arguments: The first is a comparison argument. The second is the result upon a true comparison. The third is the result upon a false comparison.
max = (max < b) ? b; In other words, assign value only if the condition is true. If the condition is false, do nothing (no assignment).
std::move(x)
Ok, so let's start by figuring out the type of std::move(snd)
. The implementation of std::move(x)
is defined to be approximately static_cast<T&&>(x)
, as per §20.2.4:
template <class T> constexpr remove_reference_t<T>&& move(T&& t) noexcept;
Returns:
static_cast<remove_reference_t<T>&&>(t)
.
which according to §5.2.9/1:
The result of the expression
static_cast<T>(v)
is the result of converting the expressionv
to typeT
. IfT
is an lvalue reference type or an rvalue reference to function type, the result is an lvalue; ifT
is an rvalue reference to object type, the result is an xvalue; otherwise, the result is a prvalue. Thestatic_cast
operator shall not cast away constness (5.2.11).
(emphasis mine)
Ok, so the returned value of std::move(snd)
is an xvalue of type printer&&
. The type of fst
is an lvalue of type const printer
.
Now, the standard describes the process to calculate the type of the resulting expression for the ternary conditional operator:
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 convert each of those operands to the type of the other. The process for determining whether an operand expression E1 of type T1 can be converted to match an operand expression E2 of type T2 is defined as follows:
- If E2 is an lvalue: E1 can be converted to match E2 if E1 can be implicitly converted (Clause 4) to the type “lvalue reference to T2”, subject to the constraint that in the conversion the reference must bind directly (8.5.3) to an lvalue.
- If E2 is an xvalue: E1 can be converted to match E2 if E1 can be implicitly converted to the type “rvalue reference to T2”, subject to the constraint that the reference must bind directly.
If E2 is a prvalue or if neither of the conversions above can be done and at least one of the operands has (possibly cv-qualified) class type:
- if E1 and E2 have class type, and the underlying class types are the same or one is a base class of the other: E1 can be converted to match E2 if the class of T2 is the same type as, or a base class of, the class of T1, and the cv-qualification of T2 is the same cv-qualification as, or a greater cv-qualification than, the cv-qualification of T1. If the conversion is applied, E1 is changed to a prvalue of type T2 by copy-initializing a temporary of type T2 from E1 and using that temporary as the converted operand.
- Otherwise (if E1 or E2 has a non-class type, or if they both have class types but the underlying classes are not the same and neither is a base class of the other): E1 can be converted to match E2 if E1 can be implicitly converted to the type that E2 would have after applying the lvalue-to- rvalue (4.1), array-to-pointer (4.2), and function-to-pointer (4.3) standard conversions.
Using this process, it is determined whether the second operand can be converted to match the third operand, and whether the third operand can be converted to match the second operand. If both can be converted, or one can be converted but the conversion is ambiguous, the program is ill-formed. If neither can be converted, the operands are left unchanged and further checking is performed as described below. If exactly one conversion is possible, that conversion is applied to the chosen operand and the converted operand is used in place of the original operand for the remainder of this section.
(again emphasis mine)
So we have two cases:
fst
and E2 is std::move(snd)
std::move(snd)
and E2 is fst
In the first case we have that E2 is an xvalue, so:
If E2 is an xvalue: E1 can be converted to match E2 if E1 can be implicitly converted to the type “rvalue reference to T2”, subject to the constraint that the reference must bind directly.
applies; but E1 (of type const printer
) cannot be implicitly converted to printer&&
due to the fact that it would lose the constness. So this conversion is not possible.
In the second case, we have that:
If E2 is an lvalue: E1 can be converted to match E2 if E1 can be implicitly converted (Clause 4) to the type “lvalue reference to T2”, subject to the constraint that in the conversion the reference must bind directly (8.5.3) to an lvalue.
applies, but E1 (std::move(snd)
of type printer&&
) can be implicitly converted to const printer&
, but does not bind directly to an lvalue; so this one also doesn't apply.
At this point we are at:
If E2 is a prvalue or if neither of the conversions above can be done and at least one of the operands has (possibly cv-qualified) class type:
section.
From which we have to consider:
if E1 and E2 have class type, and the underlying class types are the same or one is a base class of the other: E1 can be converted to match E2 if the class of T2 is the same type as, or a base class of, the class of T1, and the cv-qualification of T2 is the same cv-qualification as, or a greater cv-qualification than, the cv-qualification of T1. If the conversion is applied, E1 is changed to a prvalue of type T2 by copy-initializing a temporary of type T2 from E1 and using that temporary as the converted operand.
E1 and E2 indeed have the same underlying class types. And const printer
has a greater cv-qualification than the cv-qualification of std::move(snd)
, so the case is the one in which E1 = std::move(snd)
and E2 = fst
.
From which we finally have that:
E1 is changed to a prvalue of type T2 by copy-initializing a temporary of type T2 from E1 and using that temporary as the converted operand.
Which translated to the fact that std::move(snd)
is changed to a prvalue of type const printer
by copy-initializing a temporary of type const printer
from std::move(snd)
.
Since std::move(snd)
yields a printer&&
, the expression would be equivalent to the construction of a const printer
with printer(std::move(snd))
, which should result in printer(printer&&)
being selected.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With