左值和右值混合使用的三元运算符的类型推导

Type deduction of ternary operator with mixed lvalue and rvalue usage

最近偶然发现自己的代码是这样写的(这里简化了很多):

#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));
  }
}

有问题的方案请不要评论!这发生在一个漫长的夜晚 session... :)

我只想知道,就标准行为而言,这里实际发生了什么。三元运算符的类型推导显然会导致右值,但我想知道标准中的确切匹配短语,因为可观察到的效果是对于 idx < 1 的情况,副本的右值(? ) 的价值产生。

我读过 https://timsong-cpp.github.io/cppwp/n4659/expr.cond 但这似乎不足以解释这里的行为,因为我期望直接右值转换(尽管最初,我预计会出现编译器错误)。我还认为这里可能有一些关于此的重复,但到目前为止我无法在这里找到真正合适的。但是,如果有相当快的检测速度,请提前抱歉!

我是这样看的:

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

是一个表达式。

这个表达式的计算结果总是右值

idx < 1的情况下:这个表达式求值的右值是由copy-constructingvalue.

idx >=的情况下:这个表达式计算的右值是由move-constructingvalue构造的。

所以最后,void foo(std::string&& value) 中的函数参数值通过计算此表达式始终绑定到右值。

唯一的区别在于表达式值本身的构造方式:在第一种情况下通过复制构造,在第二种情况下通过移动构造。

根据 cppinsights.io,编译器正在创建临时 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))));

这里是将三元运算符与函数调用分开的版本:

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

cppinsights.io

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));

我试图理解阅读标准 n4835.pdf,我希望这是正确的:

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 和 4.2 不适用,所以我们来到 4.3.3。还有 7.3.1,其中 3.2 适用。

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.

请注意,7.6.16 4 说的是双向转换,所以不要过分关注 4.3(.3) 中的“E2”。