移出三元运算符的一侧
Posted
技术标签:
【中文标题】移出三元运算符的一侧【英文标题】:Moving out of one side of a ternary operator 【发布时间】:2015-07-23 03:40:47 【问题描述】:我正在写一些类似的代码:
std::string foo(bool b, const std::string& fst, std::string&& snd)
return b ? fst : std::move(snd);
然后当gcc
将其移出时,会发出叮当声并将snd
复制出来。
我试图最小化这个例子,我想出了:
#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 输出
move
clang 3.6 输出
const rvalue ref
标准是否允许 gcc 和 clang 行为?
以下随机观察:
gcc 和 clang 都将三元的类型统一为:
const printer
gcc 5.2 disassembly
clang 3.6 disassembly
【问题讨论】:
哦,太好了,我从来不知道那个网站....我喜欢组装工作。 三元运算符很复杂,通常不会按照您的逻辑预期进行。 三元运算符必须将选定的操作数转换为第二和第三操作数的通用类型。这是因为表达式的类型必须在编译时知道。它不等同于if (a) X; else Y;
。
原始代码和示例之间存在差异。原始代码具有const printer &
类型的第二个操作数,但示例将其作为const printer
。如果您可以解决此差异,它将简化您的问题
两种情况下的行为相同。
【参考方案1】:
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
转换为类型T
的结果。如果T
是左值引用类型或函数类型的右值引用,则结果为左值; 如果T
是对对象类型的右值引用,则结果是一个xvalue;否则,结果为纯右值。static_cast
运算符不应抛弃 constness (5.2.11)。
(强调我的)
好的,所以std::move(snd)
的返回值是printer&&
类型的xvalue。 fst
的类型是 const printer
类型的左值。
如何计算常用类型
现在,标准描述了为三元条件运算符计算结果表达式类型的过程:
如果第二个和第三个操作数有不同的类型并且有(可能是 cv 限定的)类类型,或者如果两者都是相同值类别和相同类型(除了 cv 限定)的 glvalue,则尝试转换这些操作数中的每一个都对应于另一个的类型。判断一个类型为 T1 的操作数表达式 E1 是否可以转换为匹配一个类型为 T2 的操作数表达式 E2 的过程定义如下:
如果 E2 是左值:如果 E1 可以隐式转换(第 4 条)为类型“对 T2 的左值引用”,则可以将 E1 转换为匹配 E2,但必须遵守在转换中引用必须直接绑定的约束( 8.5.3) 为左值。 如果 E2 是一个 xvalue:如果 E1 可以隐式转换为“对 T2 的右值引用”类型,则可以将 E1 转换为匹配 E2,但受限于引用必须直接绑定的约束。如果 E2 是纯右值,或者上述两种转换都无法完成,并且至少有一个操作数具有(可能是 cv 限定的)类类型:
如果 E1 和 E2 具有类类型,并且基础类类型相同或其中一个是另一个的基类:如果 T2 的类的类型相同或基类,则 E1 可以转换为匹配 E2 class of、T1 的类和 T2 的 cv-qualification 与 T1 的 cv-qualification 相同或更高的 cv-qualification。如果应用了转换,则通过从 E1 复制初始化 T2 类型的临时值并将该临时值用作转换后的操作数,将 E1 更改为 T2 类型的纯右值。 否则(如果 E1 或 E2 具有非类类型,或者如果它们都具有类类型但基础类不同,并且两者都不是另一个的基类):如果满足以下条件,则 E1 可以转换为匹配 E2 E1 可以隐式转换为 E2 在应用 lvalue-to- 后将具有的类型 右值 (4.1)、数组到指针 (4.2) 和函数到指针 (4.3) 标准转换。使用这个过程,确定第二个操作数是否可以转换为匹配第三个操作数,以及第三个操作数是否可以转换为匹配第二个操作数。如果两者都可以转换,或者一个可以转换但转换不明确,则程序格式错误。如果两者都不能转换,则操作数保持不变,并按如下所述执行进一步检查。如果恰好可以进行一次转换,则该转换将应用于所选操作数,并在本节的其余部分中使用转换后的操作数代替原始操作数。
(再次强调我的)
在这种情况下
所以我们有两种情况:
-
E1 是
fst
,E2 是std::move(snd)
E1 是std::move(snd)
,E2 是fst
在第一种情况下,我们有 E2 是一个 xvalue,所以:
如果 E2 是一个 xvalue:如果 E1 可以隐式转换为“对 T2 的右值引用”类型,则可以将 E1 转换为匹配 E2,但受引用必须直接绑定的约束。
适用;但是 E1(const printer
类型)不能隐式转换为 printer&&
,因为它会失去 constness。所以这种转换是不可能的。
在第二种情况下,我们有:
如果 E2 是左值:如果 E1 可以隐式转换(第 4 条)为类型“对 T2 的左值引用”,则可以将 E1 转换为匹配 E2,但必须遵守在转换中引用必须直接绑定的约束( 8.5.3) 为左值。
适用,但 E1(printer&&
类型的std::move(snd)
)可以隐式转换为const printer&
,但不直接绑定到左值;所以这个也不适用。
此时我们处于:
如果 E2 是纯右值,或者上述两种转换都无法完成,并且至少有一个操作数具有(可能是 cv 限定的)类类型:
部分。
从中我们不得不考虑:
如果 E1 和 E2 具有类类型,并且底层类类型相同或其中一个是另一个的基类:如果 T2 的类的类型相同或基类,则 E1 可以转换为匹配 E2 class of、T1 的类和 T2 的 cv-qualification 与 T1 的 cv-qualification 相同或更高的 cv-qualification。如果应用了转换,则通过从 E1 复制初始化 T2 类型的临时值并将该临时值用作转换后的操作数,将 E1 更改为 T2 类型的纯右值。
E1 和 E2 确实具有相同的基础类类型。而const printer
的 cv-qualification 比 std::move(snd)
的 cv-qualification 大,所以这种情况是 E1 = std::move(snd)
和 E2 = fst
。
我们终于得到了:
通过从 E1 复制初始化 T2 类型的临时变量并将该临时变量用作转换后的操作数,将 E1 更改为 T2 类型的纯右值。
这意味着 std::move(snd)
通过从 std::move(snd)
复制初始化 const printer
类型的临时值,更改为 const printer
类型的纯右值。
由于std::move(snd)
产生printer&&
,该表达式将等效于用printer(std::move(snd))
构造const printer
,这将导致printer(printer&&)
被选中。
【讨论】:
我可能在这个过程中错过了一个愚蠢的细节,这最终可能会改变答案,但现在是早上 7 点,我还没有完全醒来。如果有人可以检查一下,那就太好了。 太棒了,谢谢!我读了那部分并没有接近那么远,我需要提高我的标准阅读技巧。我经历了它,这对我来说似乎是正确的。 @Nick 那个部分非常难;我仍然无法弄清楚第 5 点中的“过载分辨率”是什么意思! @matt,出于好奇,你为什么要删除你的答案? @Nick 这是错误的,并且没有对讨论增加任何内容,我忽略了一点“受制于在转换中引用必须直接(8.5.3)绑定到左值的约束。”以上是关于移出三元运算符的一侧的主要内容,如果未能解决你的问题,请参考以下文章