c++c++17language-lawyercopy-elision

Is copy elision mandatory (if allowed at all) in the ternary operator?


Please consider the following C++17 code:

#include <iostream>
#include <optional>

struct S
{
    S(int) { std::cout << "S() "; }
    S(const S &) { std::cout << "S(const S &) "; }
    S(S &&) = delete;
    ~S() { std::cout << "~S() "; }
};

int main() 
{
    [[maybe_unused]] std::optional<S> v = true ? std::optional<S>(1) : std::nullopt;
}

In the latest Visual Studio 2019 16.10.3 with /std:c++latest option (C++20) it prints

S() S(const S &) ~S() ~S()

even in Release configuration with optimization.

GCC and Clang output is distinct even without optimization ( https://gcc.godbolt.org/z/ofGrzhjbc )

S() ~S()

Is copy elision optional here (all compilers are within their rights), or copy elision not allowed here (only MSVC is right), or copy elision mandatory here (only GCC and Clang are right)?


Solution

  • The conditional operator is complicated and we have to read the standard carefully to understand it. See [expr.cond].

    p4: "Otherwise, if the second and third operand have different types and either has (possibly cv-qualified) class type [...] an attempt is made to form an implicit conversion sequence from each of those operands to the type of the other. [...] If E2 is a prvalue [or ...] and at least one of the operands has (possibly cv-qualified) class type: the target type is the type that E2 would have after applying the lvalue-to-rvalue, array-to-pointer, and function-to-pointer standard conversions. Using this process, it is determined whether an implicit conversion sequence can be formed from the second operand to the target type determined for the third operand, and vice versa. If both sequences can be formed, or one can be formed but it is the ambiguous conversion sequence, the program is ill-formed. If no conversion sequence can be formed, the operands are left unchanged and further checking is performed as described below. Otherwise, if exactly one conversion sequence can be formed, 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 subclause."

    According to p4, since std::nullopt_t is implicitly convertible to std::optional<S>, the analysis continues assuming that such conversion is done (if the third operand is chosen). The implicit conversion to the non-reference target type std::optional<S> yields a prvalue of type std::optional<S>. So for the remainder of the subclause, we assume that both the second and third operands are prvalues of type std::optional<S>.

    p6: "Otherwise, the result is a prvalue. [...]"

    p7: "Lvalue-to-rvalue, array-to-pointer, and function-to-pointer standard conversions are performed on the second and third operands. After those conversions, one of the following shall hold: The second and third operands have the same type; the result is of that type and the result object is initialized using the selected operand. [...]"

    Both the second and third are already prvalues, so there is no lvalue-to-rvalue conversion to perform. They have the same type, so the result is of that type. It is a prvalue of std::optional<S>.

    Up until this point, no moves of std::optional<S> have been required. Finally, the initialization of v is subject to guaranteed copy elision so no move occurs there either. Rather, the result prvalue from the conditional expression has v as its result object, so v is simply initialized directly from the prvalue "recipe" that was the result of the conditional expression.

    You haven't said what version of MSVC you use, but its behaviour seems pretty strange to me. Obviously it doesn't implement C++17 guaranteed move elision correctly, so let's say it supports C++14. But in C++14, this code should use the move constructor, which is deleted; therefore, the program should be ill-formed. I don't see any reason why it would be allowed to copy instead.