C++11 引入的移动语义允许资源(如动态内存)在对象之间更高效地转移。它解决了深拷贝对象(尤其是大型对象)所带来的低效问题——它允许对象在可能的情况下"窃取"其他对象的资源,而不是复制它们。
在 C++11 之前,当您将一个对象赋值给另一个对象时,会调用拷贝构造函数或赋值运算符,导致对象数据的深拷贝。这个过程可能代价很高,特别是对于大型对象或包含动态内存分配的对象。该特性在性能优化至关重要的场景中特别有用,例如高性能计算、游戏开发和资源受限的环境。
移动语义引入了右值引用,以及移动构造函数和移动赋值运算符。
它的工作方式如下:
- 右值引用(
&&):右值引用允许您绑定到临时对象(右值),这些对象通常是即将被销毁或不再需要的对象。它们用双与号&&表示。 - 移动构造函数:移动构造函数是一种特殊的构造函数,它接受同类型另一个对象的右值引用,并将该对象的资源"移动"到新对象中。它通过"窃取"源对象的内部指针或句柄并将它们置空来实现,使源对象处于有效但未指定的状态。这通常使用
std::move()函数完成,它将左值转换为右值。 - 移动赋值运算符:与移动构造函数类似,移动赋值运算符允许在对象之间高效转移资源。它们接受同类型另一个对象的右值引用,并执行移动赋值操作,使源对象处于有效状态。
不过,我是否需要掌握这个特性才能受益于它显著的性能优势?幸运的是,在许多情况下,编译器会为您出色地完成工作,并尽可能地使用移动语义。
让我们看看这个基本例子:
class test
{
};
test f()
{
test t;
return t;
}
在这个示例中,开发者并没有显式使用移动特性,但编译器隐式地使用了它。确实,当函数按值返回对象时,传统上会制作对象的副本,这涉及为副本分配内存,然后将原始对象的内容复制到新分配的内存中。然而,有了移动语义,在可能的情况下,函数内局部对象所拥有的资源会被转移给被返回的对象,而不是制作副本。
我们如何检测编译器在哪些地方自动使用了这个特性?
1 - 使用 clang -cc1 -ast-dump 命令
Clang 抽象语法树(AST)转储命令是 Clang 编译器前端为 C、C++ 和 Objective-C 提供的强大工具。它允许开发者以抽象语法树的形式检查其源代码的内部表示,抽象语法树是源代码结构的层级表示。
让我们在我们的基本示例上执行这个命令
|-CXXRecordDecl 0x1ccdf79b618 <test.cpp:1:1, line:4:1> line:1:7 referenced class test definition
| |-DefinitionData pass_in_registers empty aggregate standard_layout trivially_copyable pod trivial literal has_constexpr_non_copy_move_ctor can_const_default_init
| | |-DefaultConstructor exists trivial constexpr defaulted_is_constexpr
| | |-CopyConstructor simple trivial has_const_param implicit_has_const_param
| | |-MoveConstructor exists simple trivial
| | |-CopyAssignment simple trivial has_const_param needs_implicit implicit_has_const_param
| | |-MoveAssignment exists simple trivial needs_implicit
| | `-Destructor simple irrelevant trivial needs_implicit
| |-CXXRecordDecl 0x1ccdf79b738 <col:1, col:7> col:7 implicit class test
| |-CXXConstructorDecl 0x1ccdf888b58 <col:7> col:7 implicit used constexpr test 'void () noexcept' inline default trivial
| | `-CompoundStmt 0x1ccdf889098 <col:7>
| |-CXXConstructorDecl 0x1ccdf888cc8 <col:7> col:7 implicit constexpr test 'void (const test &)' inline default trivial noexcept-unevaluated 0x1ccdf888cc8
| | `-ParmVarDecl 0x1ccdf888de8 <col:7> col:7 'const test &'
| `-CXXConstructorDecl 0x1ccdf888ec8 <col:7> col:7 implicit used constexpr test 'void (test &&) noexcept' inline default trivial
| |-ParmVarDecl 0x1ccdf888fe8 <col:7> col:7 'test &&'
| `-CompoundStmt 0x1ccdf8892a0 <col:7>
`-FunctionDecl 0x1ccdf8889d8 <line:5:1, line:9:1> line:5:6 f 'test ()'
`-CompoundStmt 0x1ccdf8892f8 <line:6:1, line:9:1>
|-DeclStmt 0x1ccdf8891f0 <line:7:5, col:11>
| `-VarDecl 0x1ccdf888ad8 <col:5, col:10> col:10 used t 'test':'test' nrvo callinit
| `-CXXConstructExpr 0x1ccdf8891c8 <col:10> 'test':'test' 'void () noexcept'
`-ReturnStmt 0x1ccdf8892e0 <line:8:5, col:12>
`-CXXConstructExpr 0x1ccdf8892b0 <col:12> 'test':'test' 'void (test &&) noexcept'
`-ImplicitCastExpr 0x1ccdf889228 <col:12> 'test':'test' xvalue <NoOp>
`-DeclRefExpr 0x1ccdf889208 <col:12> 'test':'test' lvalue Var 0x1ccdf888ad8 't' 'test':'test'
如我们所见,编译器为 test 类生成了移动构造函数和移动赋值运算符
| | |-MoveConstructor exists simple trivial
| | |-CopyAssignment simple trivial has_const_param needs_implicit implicit_has_const_param
| | |-MoveAssignment exists simple trivial needs_implicit
对于 f() 函数中的 return 语句,调用的是移动构造函数。
`-ReturnStmt 0x1ccdf8892e0 <line:8:5, col:12>
`-CXXConstructExpr 0x1ccdf8892b0 <col:12> 'test':'test' 'void (test &&) noexcept'
`-ImplicitCastExpr 0x1ccdf889228 <col:12> 'test':'test' xvalue <NoOp>
`-DeclRefExpr 0x1ccdf889208 <col:12> 'test':'test' lvalue Var 0x1ccdf888ad8 't' 'test':'test'
得益于 C++11 标准,现代编译器会在所有容易使用移动特性的情况下使用它,相比 C++11 标准之前的 C++ 编译器带来了额外的性能价值。
2 - 使用 CppDepend
CppDepend 内部使用 Clang 来解析源代码、遍历 AST,以获取计算度量所需的最大量的有用数据,并检测代码库的结构和依赖关系。
对于 test 类,我们可以在 CppDepend 类浏览器中看到生成的移动构造函数:

要查看它在哪些地方被使用,我们只需右键点击移动构造函数,并选择使用它的方法

以下是结果:

如我们所见,f() 函数隐式地使用了移动构造函数。
CppDepend 从 Clang 获取 AST,并将其转换为合适的模型,我们可以使用我们的代码查询语言对其进行查询:

如果您想知道移动语义在您的 C++ 项目中的哪些地方被使用,您可以使用这两种方法中的任何一种来进一步了解您的代码库。
