保证省略和链式函数调用

Guaranteed elision and chained function calls

假设我有以下类型:

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 中,我对这个表达式将执行多少次复制和移动有什么保证?非保证省略呢?

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

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

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

现在,让我们看一下您的表达式的一部分:a + b。由于operator+的第一个参数是取值的,所以a必须复制进去。所以这是一个副本。 return 值将被复制到 return 纯右值中。所以这是 1 份和一份 move/copy.

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

C++17 表示a + b 的纯右值return 将用于直接初始化operator+ 的参数。这不需要 copying/moving。但是 return 的值将从该参数复制。所以这是 1 个副本和 2 个 moves/copies.

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


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

"Guaranteed elision" 仅适用于 纯右值。如果它有名字,它不能是纯右值。

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

  1. 复制 a 以绑定到 lhs
  2. 将结构 lhs 从第一个 + 移出。
  3. 第一个 + 的 return 将通过省略绑定到第二个 + 的按值 lhs 参数。
  4. 第二个lhs的return会招致二次构造
  5. 第三个lhs的return会招致第三步构造
  6. 来自第三个 + 的临时 return 将在 sum 处构建。

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

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

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

导致 0 个副本和 4 个移动(7 个移动 -fno-elide-constructors)。

以上结果已通过 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 个移动。如果您愿意限制您的客户通过引用捕获您的 + 的 return,那么您可以从这样的重载之一中 return 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 不这样做的原因)。