【问题标题】:C++: rvalue references used in ternary operator seem to break existing codeC++:三元运算符中使用的右值引用似乎破坏了现有代码
【发布时间】:2013-01-24 13:24:30
【问题描述】:

我目前正在将我使用 Borland C++-Builder 5 和 6 开发多年的项目之一移植到最新的 Embarcadero C++-Builder XE 3 Update 2。XE 3 支持一些新的 C+ +11-诸如右值引用之类的东西,这对我来说当然是全新的,因为前者使用了非常旧的编译器。我只需要很少的修改就可以使我的项目可编译,但在运行时我面临一个问题,这似乎是新的右值引用和移动语义的结果。

我有一个类,其类型为 std::wstring 的字段存储一个路径,该路径只能从一个方法中读取,该方法在三元运算符中使用此字段,如下所示:

std::wstring retVal = someCondition ? this->classField : Util::doSomething(someArg);

someCondition 只是对 std::wstring.empty() 的调用,而 Util::doSomething 将 std::wstring 作为值返回,不涉及引用或其他内容,只是复制数据。

在 XE 3 中,第一次 someCondition 评估为 true 后,retVal 会正确填充 classField 的内容,但之后 classField 的内容为空。使用调试器,我可以跟踪执行到带有右值引用的优化赋值运算符:

 #if _HAS_RVALUE_REFERENCES
[...]
    _Myt& operator=(_Myt&& _Right)
        {   // assign by moving _Right
        return (assign(_STD forward<_Myt>(_Right)));
        }

    _Myt& assign(_Myt&& _Right)
        {   // assign by moving _Right
[...]

我读到的关于右值引用的内容可以完美地解释我的问题,我什至发现了两个 cmets 解释了为什么我的 classField 被视为右值。

https://stackoverflow.com/a/8535301/2055163

https://stackoverflow.com/a/6957421

我可以通过另外一份 classField 的手动副本来修复上面的行:

std::wstring retVal = someCondition ? std::wstring(this->classField) : Util::doSomething(someArg);

但这并不能解决我需要在没有编译器帮助的情况下移植的每个项目中检查三元运算符(其中混合了左值和右值)的每一个用法的问题,因为它是一个运行时可能会发生也可能不会发生的问题。

我不明白的是,我对三元运算符的使用是否有错误或不好的做法?是否有更好的解决方案来检测这些病例?为什么我应该手动复制一些对象只是为了欺骗优化?这真的是预期的行为,并且在您的大多数代码库中都没有问题吗?您如何处理这些问题?

我现在对如何取得进展有点困惑,非常感谢任何建议和/或解释。谢谢!


我测试了以下两种情况:

http://ideone.com/mWxxK3

第一个例子看起来和我的例子类似,期望没有使用类实例来存储全局字符串。 dummy2 已正确填充,并且 dummy 保留其内容。

std::wstring retVal = someCondition ? this->classField : doSomethingResult;

当我在使用三元运算符之前通过将 Util:... 的结果保存到 std::wstring 来更改上面的行以删除右值时,一切都按预期工作。 retVal 有它的内容,this->classField 也有它的内容。

现在的结论是什么? :-/

【问题讨论】:

  • 可能是编译器错误。 Util::doSomething的返回类型是什么:是按值返回还是返回引用?
  • 我按值返回,根据我引用的链接,我看到的行为是我所期望的,我只是不明白为什么这是一件“好事”。我与三元运算符一起使用的两个表达式是左值和右值,导致整个表达式被用作右值,因此应用了移动语义。
  • 你的编译器对这个程序做了什么? ideone.com/YwLjgi
  • 是的,如果没有省略,您会在 main 内部看到“复制 ctor / Move ctor / Destructor / Destructor”。但必须至少有一次复制 ctor 调用。
  • 所以听起来你没有用更简单的代码重现问题。也许其他一些代码实际上意外地踩到了this-&gt;classField?尝试减少完整程序和/或扩展 ideone 示例,直到找到答案或有 sscce.org

标签: c++ c++11 ternary-operator move-semantics rvalue-reference


【解决方案1】:

看起来像一个编译器错误。标准的相关部分是 5.16p6:

[If] 第二个和第三个操作数的类型相同;结果就是那种类型。如果操作数具有类类型,则结果是结果类型的临时纯右值,根据第一个操作数的值从第二个操作数或第三个操作数复制初始化。


如果 Util::doSomething(someArg) 返回 std::string&amp;&amp; 而不是一个值,我们需要第 5.16p3 节:

否则,如果第二个和第三个操作数具有不同的类型并且具有(可能是 cv 限定的)类类型,或者如果两者都是相同值类别和相同类型(除了 cv 限定)的泛左值,则进行尝试将这些操作数中的每一个转换为另一个的类型。判断T1类型的操作数表达式E1是否可以转换为匹配T2类型的操作数表达式E2的过程定义如下:

  • 如果E2 是左值:E1 可以转换为匹配E2 如果E1 可以隐式转换为“对T2 的左值引用”类型,受转换中的约束引用必须直接绑定到左值。
  • 如果E2 是一个xvalue:E1 可以转换为匹配E2 如果E1 可以隐式转换为“对T2 的右值引用”类型,受引用必须的约束直接绑定。
  • 如果E2 是一个右值,或者如果上述两种转换都不能完成,并且至少有一个操作数具有(可能是 cv 限定的)类类型:
    • 如果E1E2具有类类型,并且底层类类型相同或者一个是另一个的基类:E1可以转换为匹配E2如果@987654342的类@ 与 T1 的类是同一类型或基类,T2 的 cv-qualification 与 cv-qualification 相同或大于 cv-qualification T1。如果应用了转换,则通过从E1 复制初始化T2 类型的临时值并将该临时值用作转换后的操作数,将E1 更改为T2 类型的纯右值。
    • 否则(即,如果E1E2 具有非类类型,或者如果它们都具有类类型但基础类不同或不是另一个的基类):E1 可以如果E1 可以隐式转换为表达式E2E2 转换为纯右值(或它所具有的类型,如果E2 是纯右值)时所具有的类型,则转换为匹配E2

第四条适用,因此应自动制作副本,无需更改源代码。


在这两种情况下,可以在构造 retval 时安全地移动临时副本。

【讨论】:

  • 这似乎不适用;第二个和第三个操作数具有相同的类型(string)和不同的值类别(分别为 lvalue 和 prvalue),因此实现应该下降到 5.16p5,给出类型为 string 的 prvalue。
  • @catmur:不管怎样,两条规则似乎产生了相同的结果:一个不同于this-&gt;classField的临时对象。
  • 该段的第一句不成立(操作数具有相同的类型但值类别不同),因此实现没有达到重点。 Bullet 4 适用于 string a; const string b; p ? a : b;struct C {}; struct D: C {}; C c; D d; p ? c : d;
  • 另外,5.16p3 不允许双向转换,所以在类型相同的情况下用处不大。
  • 我同意@ecatmur:第 5 段和第 6 段适用于此,而不是第 3 段。
猜你喜欢
  • 2021-04-15
  • 2022-01-04
  • 2020-02-22
  • 2011-02-23
  • 2019-08-07
  • 1970-01-01
  • 1970-01-01
  • 2011-08-13
  • 2012-06-23
相关资源
最近更新 更多