使用constexpr实现更快速、更小巧、更安全的代码

本文探讨了C++14中constexpr关键字的强大功能,它能够在编译时计算常量表达式,提升代码性能、减小可执行文件体积,并通过防止未定义行为增强代码安全性。文章还介绍了基于libclang的自动化工具constexpr-everything。

使用constexpr实现更快速、更小巧、更安全的代码

随着C++14的发布,标准委员会加强了C++最酷的现代特性之一:constexpr。现在,C++开发者可以编写常量表达式并强制在编译时而非每次用户调用时进行评估。这带来了更快的执行速度、更小的可执行文件,以及出人意料的是,更安全的代码。

未定义行为一直是许多安全漏洞的根源,例如Linux内核权限提升(CVE-2009-1897)以及无数因未定义行为而被移除的错误实现的整数溢出检查。C++标准委员会在设计constexpr时决定,标记为constexpr的代码不能调用未定义行为。

constexpr面对未定义行为

最近在我们的内部Slack频道中,一位同事试图创建一个可利用的二进制文件,其中漏洞是一个未初始化的栈局部变量,但他正在与编译器斗争。编译器拒绝生成易受攻击的代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* clang -o example example.cpp -O2 -std=gnu++14 \
   -Wall -Wextra -Wshadow -Wconversion
 */
typedef void (*handler)();

void handler1();
void handler2();
void handler3();

handler handler_picker(int choice) {
    handler h;
    switch(choice) {
    case 1:
        h = handler1;
        break;
    case 2:
        h = handler2;
        break;
    case 3:
        h = handler3;
        break;
    }

    return h;
}

使用现代编译器(clang 8.0)编译示例代码时,编译器会静默消除易受攻击的情况。如果调用者指定了switch未处理的选择(如0或4),函数将返回handler2。这在优化级别大于-O0时成立。

原因当然是未定义行为。由于未定义行为不能存在,编译器可以自由地对代码做出假设——在这种情况下假设handler h永远不会未初始化。

现在编译器静默接受这段糟糕的代码,只是假设我们知道自己在做什么。理想情况下,它应该报错。这就是constexpr拯救我们的地方。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
constexpr handler handler_picker(int choice)
{
    handler h;
    switch(choice) {
    case 1:
        h = handler1;
        break;
    case 2:
        h = handler2;
        break;
    case 3:
        h = handler3;
        break;
    }

    return h;
}

编译器错误:

1
2
3
4
5
6
<source>:9:13: error: variables defined in a constexpr 
function must be initialized
    handler h;
            
1 error generated.
Compiler returned: 1

constexpr在这里强制产生错误,这正是我们想要的。它对大多数形式的未定义行为都有效,但编译器实现中仍然存在差距。

constexpr一切!

在深入研究clang源代码后,我意识到可以使用libclang在语义分析期间确定某物是否可以constexpr的相同机制,自动将函数和方法标记为constexpr。

我最初开始编写一个clang-tidy传递,但在可用API和传递中的上下文方面遇到了问题。我决定创建自己的独立工具:constexpr-everything。它可在我们的GitHub上获得,并应与最近的libclang版本配合使用。

我编写了两个访问器,一个试图识别函数是否可以标记为constexpr。这变得相当简单;我迭代当前翻译单元中的所有clang::FunctionDecls,并使用clang::Sema::CheckConstexprFunctionDecl、clang::Sema::CheckConstexprFunctionBody和clang::Sema::CheckConstexprParameterTypes询问它们是否可以在constexpr上下文中评估。

应用constexpr变量的困难

我们需要将变量标记为constexpr以强制评估constexpr函数。自动将constexpr应用于函数很容易。对变量这样做相当困难。在尝试尽可能广泛地应用constexpr并与我的测试用例斗争后,我切换了策略,采用了更保守的方法:只标记已经具有const限定符并具有constexpr初始化器或构造函数的变量。

在你的代码库上尝试

在运行constexpr-everything之前和之后对测试进行基准测试。不仅你的代码会更快、更小,而且会更安全。标记为constexpr的代码不容易发生bitrot。

constexpr-everything仍然是一个原型——它还有一些粗糙的边缘。最大的问题是FixIts只应用于源(.cpp)文件,而不应用于它们的关联头文件。此外,constexpr-everything只能将现有的constexpr兼容函数标记为constexpr。我们正在努力使用提供的机制来识别由于未定义行为而无法标记的函数。

代码可在我们的GitHub上获得。要自己尝试,你需要cmake、llvm和libclang。试试看,并告诉我们它对你的项目如何工作。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计