Notes on C++ Copy Elision

Omits copy and move (since C++11) constructors, resulting in zero-copy pass-by-value semantics.

以上是 copy elision 的解释。

下面列举一些代码:

1
2
3
4
5
6
7
8
9
void get_F1(S& s) {
s.i = 8;
}

S* get_F2() {
S* ps = new S; // 2. default ctor 2
ps->i = 8;
return ps; // should be freed later
}

1
2
3
4
5
T get_object(args...) {
T t;
// ...
return std::move(t);
}

至少一般我们被教育过,不要 return std::move(t), 同时我们知道,返回某个 S 类型的量,可能会有 Copy Elision 的优化,帮助我们就地在外部构造这个值 (constructing the automatic object directly into the exception object.)

Value Category and Guaranteed Copy Elision

C++11 定义了 Value Categories. C++ 中,每个 expression 都有一个 value category

我们考虑仅定义 lvaluervalue. 字面量 2 肯定是 rvalue, 而 lvalue 被视作 localizable value, 那么,简单句几个例子:

  • v 是一个 std::vector, v.front() 是一个 lvalue
  • *p 是一个 lvalue
  • 甚至一个字符串字面量都是一个不可更改值的 lvalue.

nullptr, 'a', 7 这些被视作 rvalue.

lvalue 可以被转成 rvalue ,所以 y = x 是可以的,当然 7 = 8 就不行啦。

上面只是一个非常模糊的说明,C++11 定义了下面的内容:

BE725B3AB0EF7D38B52D06B194606D7D

Guaranteed copy elision through simplified value categories 定义了新的 value category, 随后又了如下变更:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0135r1.html

这意味着,如果你有一个

1
2
3
4
5
6
T f() {
return T();
}

T x = f(); // 1
T x2 = T(T(f())); // 2

这里需要根据 f() 的上下文推断,T() 是个 prvalue, 那么 C++ 17 会强制 1 仅调用一次 T 的构造函数

然后,再看 2:

If the initializer expression is a prvalue and the cv-unqualified version of the source type is the same class as the class of the destination, the initializer expression is used to initialize the destination object. [ Example**: T x = T(T(T())); calls the T default constructor to initialize x. ]**

所以这里构造函数也只会走一次。

以上内容在 C++17 之后是强制的。

NRVO

我们再考虑 NRVO:

1
2
3
4
5
T get_object(args...) {
T t;
// ...
return std::move(t);
}

我们贴两个图:

CFF34BB9BC32A9B1EA0A3C8660C537B0

628BAC04ED66DC742323A690115292E2

这里描述了 NRVO 和 NRVO 失败的情况。可以看到,对于一个 HardToCopyAndEasyToMove 的结构,即使不 return std::move(..), 编译器也能正确的优化

遇上 scoped_guard

如果你想在参数返回之前,做一些检查,在 go 里面,你没准这么写了:

1
2
3
4
5
6
7
8
9
10
11
var s Status // when init s.ok() == true
defer func() {
if s.ok() {
...
} else {
...
}
}

// ...
return s

不谈代码好不好,这里逻辑上是可以的,因为 Go 万物都是 Copy。

当你想在 C++ 里面实现这些东西的时候,比如:

1
2
3
4
5
Status s; // when moved, it will be ok
std::scoped_guard defer_fn; // check s
...

return s;

如果这里走了 move, 那我们有 evaluation order: https://en.cppreference.com/w/cpp/language/eval_order

BA42F5DB-AE8A-49C9-8FF4-946D1A2BCB2B

我们可以看到,这里可能先发生 move, 再来 check s, move 之后 s 可能就不能满足用户的预期了。

感想

其实我也不懂 C++,有什么地方写错了,请立刻通知我,我会第一时间查证和改正。

查阅的时候,虽然感觉 C++ 规则很复杂,但是大部分时候,即使不熟悉这些细节,正常写代码也能保证高效率。正如没有受过法律的正常人,也很少会犯法。

好了,搞完了,接着打逆转裁判了。

References