Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Moving out of one side of a ternary operator

Tags:

c++

gcc

c++11

clang

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

like image 404
Nick Avatar asked Jul 23 '15 03:07

Nick


People also ask

How do you break in a ternary operator?

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.

What are the 3 operands in ternary operator?

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.

What are the three arguments of a ternary operator?

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.

How do you use only if condition in ternary operator?

max = (max < b) ? b; In other words, assign value only if the condition is true. If the condition is false, do nothing (no assignment).


1 Answers

The type of 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 expression v to type T. If T is an lvalue reference type or an rvalue reference to function type, the result is an lvalue; if T is an rvalue reference to object type, the result is an xvalue; otherwise, the result is a prvalue. The static_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.


How to calculate the common type

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)


In this context

So we have two cases:

  1. E1 is fst and E2 is std::move(snd)
  2. E1 is 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.

like image 180
Shoe Avatar answered Sep 19 '22 16:09

Shoe