【问题标题】:Guaranteed elision and chained function calls保证省略和链式函数调用
【发布时间】:2017-02-10 18:04:17
【问题描述】:

假设我有以下类型:

struct X {
    X& operator+=(X const&);
    friend X operator+(X lhs, X const& rhs) {
        lhs += rhs;
        return lhs;
    }
};

我有声明(假设所有命名变量都是X类型的左值):

X sum = a + b + c + d;

在 C++17 中,我对这个表达式将执行多少个副本和移动有什么保证?非保证省略呢?

【问题讨论】:

    标签: c++ c++17 copy-elision


    【解决方案1】:

    这将执行 1 次复制构造和 3 次移动构造。

    1. 复制a 以绑定到lhs
    2. 将构造 lhs 移出第一个 +
    3. 第一个 + 的返回值将绑定到第二个 + 的按值 lhs 参数,并带有省略号。
    4. 第二个lhs 的返回将引发第二个移动构造。
    5. 第三个lhs 的返回将导致第三个移动构造。
    6. 从第三个+ 返回的临时值将在sum 构造。

    对于上面描述的每个移动结构,都有另一个移动结构可以选择省略。所以你只有保证有 1 个副本和 6 个动作。但在实际操作中,除非你-fno-elide-constructors,否则你将有1个副本和3个动作。

    如果你在这个表达式之后没有引用a,你可以进一步优化:

    X sum = std::move(a) + b + c + d;
    

    产生 0 个副本和 4 个移动(-fno-elide-constructors 的 7 个移动)。

    上面的结果已经通过X 得到证实,该X 检测了复制和移动构造函数。


    更新

    如果您对优化此功能的不同方法感兴趣,可以从重载 X const&X&& 上的 lhs 开始:

    friend X operator+(X&& lhs, X const& rhs) {
        lhs += rhs;
        return std::move(lhs);
    }
    friend X operator+(X const& lhs, X const& rhs) {
        auto temp = lhs;
        temp += rhs;
        return temp;
    }
    

    这将事情减少到 1 个副本和 2 个移动。如果您愿意限制您的客户通过引用获得 + 的返回,那么您可以从以下重载之一返回 X&&

    friend X&& operator+(X&& lhs, X const& rhs) {
        lhs += rhs;
        return std::move(lhs);
    }
    friend X operator+(X const& lhs, X const& rhs) {
        auto temp = lhs;
        temp += rhs;
        return temp;
    }
    

    让您减少 1 个副本和 1 个移动。请注意,在此最新设计中,如果您的客户曾经这样做过:

    X&& x = a + b + c;
    

    那么x 是一个悬空引用(这就是std::string 不这样做的原因)。

    【讨论】:

    • 所以没有省略链?例如将a+b的返回直接构造成operator+(??, c)?
    • 我什至认为忽略lhs 的返回是不合法的。然而,我也不认为任何编译器作者有任何动机使其合法(反正没有人尝试)。
    • 为什么它可能是非法的?以后能不能合法化?
    • @Orient:我想说的是标准不允许这样做。据我所知,没有人提出过它。我怀疑其原因是没有人想出如何实施它。但这些都是猜测,我不是编译器专家。
    【解决方案2】:

    好的,让我们从这个开始:

    X operator+(X lhs, X const& rhs) {
        lhs += rhs;
        return lhs;
    }
    

    这将总是引发从参数到返回值对象的复制/移动。 C++17 没有改变这一点,任何形式的省略都无法避免这种复制。

    现在,让我们看一下您的表达式的一部分:a + b。由于operator+ 的第一个参数是按值取值的,所以a 必须复制 到其中。所以这是一个副本。返回值将被复制到返回纯右值中。所以这是 1 个副本和一个移动/副本。

    现在,下一部分:(a + b) + c

    C++17表示a + b返回的prvalue会直接用来初始化operator+的参数。这不需要复制/移动。但是这个的返回值将从那个参数复制。所以这是 1 个副本和 2 个移动/副本。

    对最后一个表达式重复此操作,即 1 次复制和 3 次移动/复制。 sum 将从纯右值表达式初始化,因此不需要在那里进行复制。


    您的问题似乎真的是参数 remain 是否从 C++17 中的省略中排除。因为they were already excluded in prior versions。这不会改变;从省略中排除参数的原因尚未失效。

    “保证省略”仅适用于纯右值。如果它有名字,它不能是prvalue。

    【讨论】:

    • 其中一些将是移动 - 你的意思是 1 复制和 3 移动?
    • 把 Nicol 关于参数副本的评论换一种说法:按值取参数仅有助于在输入的过程中进行复制省略,仅在消费时才有意义 i> 参数,即std::move()ing 在别处,例如:X rc(std::move(lhs)); rc += rhs; return rc;
    • @Barry:是的,我忘了返回局部变量的动作是自动的。
    • 我想如果这整件事可以编译成 X sum(a); sum += b; sum += c; sum += d; 的等价物,那就太酷了。猜猜这仍然很难。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-02-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-10-28
    相关资源
    最近更新 更多