保证省略和链式函数调用
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 次移动构造。
- 复制
a
以绑定到 lhs
。
- 将结构
lhs
从第一个 +
移出。
- 第一个
+
的 return 将通过省略绑定到第二个 +
的按值 lhs
参数。
- 第二个
lhs
的return会招致二次构造
- 第三个
lhs
的return会招致第三步构造
- 来自第三个
+
的临时 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
不这样做的原因)。
假设我有以下类型:
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 次移动构造。
- 复制
a
以绑定到lhs
。 - 将结构
lhs
从第一个+
移出。 - 第一个
+
的 return 将通过省略绑定到第二个+
的按值lhs
参数。 - 第二个
lhs
的return会招致二次构造 - 第三个
lhs
的return会招致第三步构造 - 来自第三个
+
的临时 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
不这样做的原因)。