From this example:
static_assert(std::same_as<decltype(true ? std::string{} : "str"), std::string>); // compiles
You can see that result of conditional expression is prvalue of std::string
. But I can't explain it according to the standard.
Here's my two possibilities:
#1:
First operand is prvalue of class type, and there is no base and derived types, so expr.cond#4.3.3 sub-sub-clause applied, which states that target type
(not result type yet) should be std::string
. I know that this clause says:
...lvalue-to-rvalue, array-to-pointer, and function-to-pointer standard conversions.
But this is why this explanation is wrong, but if we assume that std::string{}
operand gives us the target type
of std::string
(somehow, with this clause) and because there is no way to convert std::string
to const char[4]
(so expr.cond#4.6 satisfied), it means that implicit conversion sequence for second operand is formed to std::string
and it is: array-to-pointer conversion (const char[4] -> const char*
), and then user-defined converting constructor of std::string
that takes const char*
.
So now we go next to expr.cond#5, which is not satisfied, because we have both prvalues of std::string
, so expr.cond#6 is used and going through rest of it we have our result type of prvalue of std::string
.
#2:
This expr.cond#4.3.3 doesn't work, exactly because first operand is prvalue and lvalue-to-rvalue conversion couldn't apply. And this expr.cond#5 also doesn't. So we left with expr.cond#6, but it says:
...overload resolution is used to determine the conversions (if any) to be applied to the operands ([over.match.oper], [over.built]). ...
These over.match.oper and over.built, as far as I can tell, only about overloaded operators (doesn't matter in this issue) and conversion functions (which std::string
has none) to built-in types. And in general, I have feeling, that this whole clause only about converting class types to built-in types, so no converting constructors.
Which, again, makes this explanation wrong. But if we assume that in expr.cond#6 converting constructors could be called (like the one that std::string
has) then result is prvalue of std::string
.
Note #1: version of draft that I was linking, in this clause expr.cond#7, mentions only:
Array-to-pointer and function-to-pointer standard conversions...
But C++20 draft in same clause have also "lvalue-to-rvalue" conversion. I'm curious why such change was made, even though something like this operator int&();
in expr.cond#6 could make one of the operands lvalue. Might help with the issue.
Note #2: this question related to this one in a way such that answer there is incomplete a little bit, because it doesn't mention anything about user-defined conversions in this step expr.cond#6. And these conversions probably also subtle point here. Maybe thinking about example there will help.
The reason why conditional operator yields prvalue of std::string
is because of:
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 from each of those operands to the type of the other.
And first operand has class type.
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
std::string{}
is prvalue, so this is relevant case.
otherwise, 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.
Operands are not of the same type (so [expr.cond] p4.3.1 does not apply) and are not base and derived classes (so [expr.cond] p4.3.2 also does not apply). And the target type
is std::string
, i.e. E2
left unchanged.
Note: E2
is std::string{}
, so none of the conversions above are applicable. But there actually no reason why at least one of conversion should be applicable, in terms of wording. The sentence doesn't start with "If", so there is no constraints on E2
, so it could be prvalue. And obviously all three conversions shouldn't be applicable.
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.
Only one conversion sequence can be formed, because other conversion sequence would be from std::string
to some type related to const char[4]
, and class std::string
doesn't have any conversion member functions. And that's why we doesn't care about E2=const char[4]
case for above clauses.
The second and third operands have the same type; the result is of that type and the result is copy-initialized using the selected operand.
After implicit conversion both operands are prvalues of the same type std::string
, so [expr.cond] p5 doesn't apply here, also they have same type, so [expr.cond] p6 also doesn't apply. And result type is prvalue of std::string
.
Also if you curios why C++20 draft doesn't have lvalue-to-rvalue conversion as I mentioned in a question, that's why: CWG2906. But it has nothing to do with the issue.