【问题标题】:Conditional move or copy assignment in ternary operator三元运算符中的条件移动或复制赋值
【发布时间】:2019-03-31 20:18:46
【问题描述】:

对于下面的代码sn-p:

#include <utility>
#include <iostream>

#define C(name) (name ? name : "nullptr")
#define PP { std::cout << __PRETTY_FUNCTION__ << " : " << C(name) << '\n'; }
#define T { std::cout << __PRETTY_FUNCTION__ << " : " << C(name) << " -> " << C(rhs.name) << '\n'; }

struct A
{
    const char * name = nullptr;
    A(const char * name) : name{name} PP
    A(A && rhs) : name{std::exchange(rhs.name, nullptr)} PP
    A(const A & rhs) : name{rhs.name} PP
    A & operator = (A && rhs) { T; std::swap(name, rhs.name); return *this; }
    A & operator = (const A && rhs) { T; name = rhs.name; return *this; }
    ~A() PP
};

#include <random>

int main()
{
    std::random_device d;
    A a{"a"};
    A b{"b"};
    A c{"c"};
    std::cout << "begin\n";
    a = ((d() % 2) == 0) ? b : std::move(c);
    std::cout << "end\n";
}

可能有以下两种输出:

A::A(const char*) : a
A::A(const char*) : b
A::A(const char*) : c
begin
A::A(A&&) : c
A& A::operator=(A&&) : a -> c
A::~A() : a
end
A::~A() : nullptr
A::~A() : b
A::~A() : c

A::A(const char*) : a
A::A(const char*) : b
A::A(const char*) : c
begin
A::A(const A&) : b
A& A::operator=(A&&) : a -> b
A::~A() : a
end
A::~A() : c
A::~A() : b
A::~A() : b

在上述情况下,编译器是否可以(根据标准)避免在使用三元运算符进行复制/移动赋值期间使用临时值,并分派到复制或移动赋值运算符以分配右侧的值 ( bc) 直接到左侧 (a) 取决于条件?

【问题讨论】:

  • 你的意思是if ((d() % 2) == 0) a = b; else a = std::move(c);
  • @Evg 是的。有点

标签: c++ language-lawyer ternary-operator move-semantics value-categories


【解决方案1】:

是否有可能...编译器在使用三元运算符进行复制/移动赋值时避免使用临时值

由于您编写代码的方式,这是一个有趣的问题。

一般中,是的。允许编译器重新排列或删除代码只要可观察的结果与执行代码时相同。这称为假设规则。

编译器还可以在其他情况下删除副本,即使观察到的行为会发生变化,例如RVO(返回值优化)。

但是,在您的情况下,所有构造函数都具有无法更改的可观察行为 - 它们将字符发送到标准输出!

因此,在这种特殊情况下,编译器别无选择,只能遵循原始代码的流程。

【讨论】:

    【解决方案2】:

    如果我们查看标准草案[class.copy.elision] 中关于复制/移动省略的部分,其中涵盖了复制/移动即使有副作用也可以省略的情况。我们没有看到任何涵盖您的示例的案例:

    当满足某些条件时,允许实现省略类的复制/移动构造 对象,即使为复制/移动操作选择了构造函数和/或对象的析构函数 有副作用。在这种情况下,实现会处理省略的复制/移动的源和目标 操作只是引用同一对象的两种不同方式。如果选中的第一个参数 构造函数是对对象类型的右值引用,该对象的销毁发生在目标 会被摧毁;否则,破坏发生在两个对象的较晚时间 没有优化就会被破坏。119 这种复制/移动操作的省略,称为复制 省略,在以下情况下是允许的(可以组合消除多个副本):

    • 在具有类返回类型的函数的 return 语句中,当表达式是 非易失性自动对象(函数参数或由 与函数具有相同类型(忽略 cv 限定)的处理程序的异常声明(13.3)) 返回类型,直接构造自动对象可以省略复制/移动操作 进入函数调用的返回对象
    • 在 throw 表达式 (7.6.17) 中,当操作数是非易失性自动对象的名称时 (函数或 catch 子句参数除外),其范围不超出 最里面的封闭 try 块(如果有),从操作数复制/移动操作到 可以通过将自动对象直接构造到异常中来省略异常对象(13.1) 对象
    • 当异常处理程序的异常声明(第 13 条)声明相同的对象时 类型(除了 cv-qualification)作为异常对象(13.1),复制操作可以省略 如果程序的含义将异常声明视为异常对象的别名 除了为由 异常声明。 [注意:不能从异常对象移动,因为它始终是 左值。 ——尾注]

    在需要常量表达式的上下文中计算表达式时需要复制省略 (7.7) 并在不断的初始化(6.8.3.2)。 [注意:如果是相同的表达式,可能不会执行复制省略 在另一种情况下进行评估。 ——尾注]

    【讨论】:

      猜你喜欢
      • 2011-03-06
      • 2017-11-21
      • 2017-07-16
      • 2021-04-19
      • 2010-09-05
      • 2021-08-27
      • 2020-05-16
      • 2011-06-24
      相关资源
      最近更新 更多