从三元运算符的一侧移出

Nic*_*ick 12 c++ gcc clang c++11

我写的代码类似于:

std::string foo(bool b, const std::string& fst, std::string&& snd) {
  return b ? fst : std::move(snd);
}
Run Code Online (Sandbox Code Playgroud)

把它扯snd出来并抄gcc出来.我试图最小化这个例子,我想出了:

#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);
}
Run Code Online (Sandbox Code Playgroud)

gcc 5.2输出

move
Run Code Online (Sandbox Code Playgroud)

clang 3.6输出

const rvalue ref
Run Code Online (Sandbox Code Playgroud)

标准是否同时允许gcc和clang行为?

随机观察如下:

gcc和clang都将三元的类型统一为:

const printer
Run Code Online (Sandbox Code Playgroud)

gcc 5.2反汇编

clang 3.6拆卸

Sho*_*hoe 11

的类型 std::move(x)

好吧,让我们首先弄清楚它的类型std::move(snd).根据§20.2.4,实现std::move(x)被大致定义为static_cast<T&&>(x):

template <class T> constexpr remove_reference_t<T>&& move(T&& t) noexcept;

返回:static_cast<remove_reference_t<T>&&>(t).

根据§5.2.9/ 1:

表达式static_cast<T>(v)的结果是将表达式转换v为type 的结果T.如果T是左值引用类型或函数类型的右值引用,则结果为左值; if T是对象类型的右值引用,结果是xvalue ; 否则,结果是prvalue.该static_cast经营者不得抛弃常量性(5.2.11).

(强调我的)

好的,所以返回的值std::move(snd)是类型的xvalue printer&&.类型fst是左值类型const printer.


如何计算常见类型

现在,该标准描述了计算三元条件运算符的结果表达式类型的过程:

如果第二个和第三个操作数具有不同的类型并且具有(可能是cv限定的)类类型,或者两者都是相同值类别的glvalues和除cv-qualification之外的相同类型,则尝试转换每个操作数操作数与另一种类型的操作数.确定类型T1的操作数表达式E1是否可以转换为匹配类型T2的操作数表达式E2的过程定义如下:

  • 如果E2是左值:如果E1可以被隐式转换(第4条)到类型"左值引用T2",则E1可以被转换为匹配E2,受制于转换中引用必须直接绑定的约束(8.5.3) )到左值.
  • 如果E2是x值:如果E1可以隐式转换为"rvalue reference to T2"类型,则E1可以转换为匹配E2,受限于引用必须直接绑定.
  • 如果E2是prvalue,或者上面的转换都不能完成,并且至少有一个操作数具有(可能是cv-qualified)类类型:

    • 如果E1和E2具有类类型,并且底层类类型相同或者一个是另一个类的基类:如果T2的类与类的类型相同,则可以转换为E1,或者基类类, T1的类别和T2的cv资格是与cv资格相同的cv资格或更高的cv资格.如果应用转换,则通过从E1复制初始化T2类型的临时值并将该临时值用作转换后的操作数,将E1更改为类型T2的prvalue.
    • 否则(如果E1或E2具有非类型类型,或者它们都具有类类型但基础类不相同且两者都不是另一类的基类):如果E1可以是E1,则可以将E1转换为匹配E2在应用左值到右值(4.1),数组到指针(4.2)和函数到指针(4.3)标准转换后,隐式转换为E2将具有的类型.

使用该过程,确定是否可以转换第二操作数以匹配第三操作数,以及是否可以转换第三操作数以匹配第二操作数.如果两者都可以转换,或者一个可以转换,但转换不明确,则程序格式不正确.如果两者都不能被转换,则操作数保持不变并且如下所述执行进一步检查.如果只能进行一次转换,则将该转换应用于所选操作数,并使用转换后的操作数代替本节其余部分的原始操作数.

(再次强调我的)


在这种背景下

所以我们有两种情况:

  1. E1是fst和E2是std::move(snd)
  2. E1是std::move(snd)和E2是fst

在第一种情况下,我们有E2是xvalue,所以:

如果E2是x值:如果E1可以隐式转换为"rvalue reference to T2"类型,则E1可以转换为匹配E2,受限于引用必须直接绑定.

适用; 但是E1(类型const printer)不能被隐式转换为printer&&由于它会丢失常量的事实.所以这种转换是不可能的.

在第二种情况下,我们有:

如果E2是左值:如果E1可以被隐式转换(第4条)到类型"左值引用T2",则E1可以被转换为匹配E2,受制于转换中引用必须直接绑定的约束(8.5.3) )到左值.

适用,但E1(std::move(snd)类型printer&&)可以隐式转换为const printer&,但不直接绑定到左值; 所以这个也不适用.

在这一点上,我们在:

如果E2是prvalue,或者上面的转换都不能完成,并且至少有一个操作数具有(可能是cv-qualified)类类型:

部分.

我们必须从中考虑:

如果E1和E2具有类类型,并且底层类类型相同或者一个是另一个类的基类:如果T2的类与类的类型相同,则可以转换为E1,或者基类类, T1的类别和T2的cv资格是与cv资格相同的cv资格或更高的cv资格.如果应用转换,则通过从E1复制初始化T2类型的临时值并将该临时值用作转换后的操作数,将E1更改为类型T2的prvalue.

E1和E2确实具有相同的基础类类型.并且const printer具有比cv资格更大的cv资格std::move(snd),因此情况是E1 = std::move(snd)和E2 = fst.

从中我们最终得到了:

通过从E1复制初始化T2类型的临时值并将该临时值用作转换后的操作数,将E1更改为T2类型的prvalue.

其转换到这一事实std::move(snd)改为类型的prvalue const printer通过复制初始化一个临时的类型const printerstd::move(snd).

由于std::move(snd)收益率printer&&,表达将相当于的建设const printerprinter(std::move(snd)),这将导致printer(printer&&)被选择.