C++ 编译器有时必须创建临时对象。它们会在以下情况中使用:
- 引用初始化。
- 包含标准类型转换的表达式求值。
- 实参传递。
- 函数返回。
- throw 表达式求值。
对于非平凡(non-trivial)的类,临时对象的创建和销毁在处理时间和内存占用方面都可能代价不菲。在这种情况下,您应当尽量减少它们的产生。C++ 编译器确实会消除一些临时对象,但它无法消除全部。
要检测临时对象在何处产生并不总是那么容易,正如 Herb Sutter 在他的示例中所解释的。编译器掌握这些信息,但我们能否从编译器那里获得临时对象产生的位置?如果不能,修改它来报告这些信息容易吗?
在我们的案例中,我们将使用 Clang:它非常灵活,提供了许多定制其行为的方式。事实上,Clang 的一个主要设计理念就是采用基于库的架构。在这种设计中,前端的各个部分可以被干净地划分成独立的库,然后根据不同的需求和用途进行组合。此外,基于库的方法鼓励设计良好的接口,也让新开发者更容易参与进来(因为他们只需要理解大局中的一小部分)。
Clang 编译器分为三个阶段:
- 前端:解析源代码、检查错误,并构建特定语言的抽象语法树(AST)来表示输入代码。
- 优化器:其目标是对前端生成的 AST 执行一些优化。
- 后端:生成最终由机器执行的代码;它取决于目标平台。

在我们的案例中,我们将聚焦于前端阶段。目标是获得一个源文件的抽象语法树(AST),并检查它是否报告了关于临时对象的有用信息。
让我们来看看这段最小化源代码的 AST:

要生成 AST,我们可以使用 -cc1 开关执行 Clang 前端解析器。
clang -cc1 -ast-dump test.cpp
下面是为 GetTest 函数生成的 AST:

在这份 AST 中,有两条信息与临时对象相关:nrvo和elidable。
具名返回值优化(NRVO)是一种编译器优化技术,旨在消除为保存函数返回值而创建的临时对象。NRVO 消除了基于栈的返回值的复制构造函数和析构函数调用,从而优化掉冗余的构造与析构调用,提升整体性能。更多细节可参阅它的维基页面。
复制消除(copy elision)是一种编译器优化技术,可以消除不必要的对象复制。更多细节可参阅它的维基页面。
有趣的是,Clang 会告诉我们在哪里应用了 NRVO,但它并不显式报告临时对象在何处创建。让我们深入 AST 导出器(AST dumper)的源代码,试着把这些信息报告出来。
ASTDumper 是如何工作的?
编译器解析程序,并把解析后的程序表示为一棵抽象语法树(AST)。AST 有许多不同种类的节点,比如赋值节点、变量引用节点和算术表达式节点。生成 AST 之后,Clang 会调用一些遍历它并执行处理的前端动作;ASTDumpAction 就是其中之一,它把 AST 转储到控制台。
ASTDumper 是这样声明的:
class ASTDumper
: public ConstDeclVisitor, public ConstStmtVisitor,
public ConstCommentVisitor
当我们需要遍历一个结构、并对该结构的每个节点应用特定处理时,访问者模式是推荐使用的模式。
下面是遍历 AST 时被调用的一些方法;每一个都对应一种特定的 AST 节点。
void VisitNamespaceDecl(const NamespaceDecl *D);
void VisitUsingDirectiveDecl(const UsingDirectiveDecl *D);
void VisitNamespaceAliasDecl(const NamespaceAliasDecl *D);
void VisitTypeAliasDecl(const TypeAliasDecl *D);
void VisitTypeAliasTemplateDecl(const TypeAliasTemplateDecl *D);
void VisitCXXRecordDecl(const CXXRecordDecl *D);
void VisitStaticAssertDecl(const StaticAssertDecl *D);
改造 Clang 的 AST 导出器
我们的目标是报告被创建的临时对象,因此需要跟踪对象的创建,并识别每个对象由哪个 AST 节点参与构造。
在 Clang 中,当需要创建一个 C++ 对象时,会调用 VisitCXXConstructExpr;下面是它的实现:

可以看到,代码中并没有检查被创建的对象是否为临时对象;但好消息是,CXXConstructExpr 有一个 IsTemporaryObject 方法,可以判断该表达式的结果是否为给定类类型的临时对象。
让我们修改实现,加上另一个条件:

下面是修改后生成的新的 AST:

只需添加几行代码,ASTDumper 现在就能显式报告临时对象在何处创建了。
结语
LLVM/Clang 不仅仅是一个编译器,它还是一套用于开发您自己的 C/C++/Objective-C 工具的强大基础设施。它的架构相对容易理解,也可以根据您的需求进行定制。别犹豫,下载 Clang 源代码,做一些修改并重新构建——这对想学习编译器工作原理的学生尤其有帮助。
