constexpr
函数是如何在编译期执行的?constexpr
函数局部变量以及循环是如何实现?constexpr if
是什么原理?或许你有不少疑惑,但目前关于constexpr
编译期的资料还比较少,本文笔者将从 gcc 的源码简单分析一下,以此来抛砖引玉,让更多的人加入到对constexpr
的讨论中。
我用的是最新的gcc 11源码,直接翻到gcc/gcc/cp/constexpr.cc
,在这个文件里我们可以看到gcc是如何解析constexpr
的。
我们先介绍几个重要的数据结构
tree
是gcc内部的语法树。
constexpr_call
是gcc内部对constexpr函数的包装,它将函数定义、函数参数以及函数结果打包在一起,以便进行编译期求值。可将其和标准库的std::function
类比一下,具体结构如下
struct constexpr_call {
/* constexpr函数定义 */
constexpr_fundef *fundef;
/* 打包的constexpr函数参数 */
tree bindings;
/* 函数调用结果
1. NULL表示constexpr函数正在被求值
2. error_mark_node表示求值出现错误
3. 其他值则表示正确的调用结果 */
tree result;
...
};
constexpr_global_ctx
是全局constexpr上下文,每个cxx_eval_outermost_constant_expr(),即最外层的constexpr调用都需要有一个全局constexpr上下文,保存着表达式内部变量到到初始化值的映射,具体结构如下
struct constexpr_global_ctx {
/* 保存常量表达式内部的局部变量或临时变量 */
hash_map<tree,tree> values;
/* 在计算最外层常量表达式时创建的堆 */
auto_vec<tree, 16> heap_vars;
/* 待释放的堆数量 */
unsigned heap_dealloc_count;
...
};
constexpr_ctx
是constexpr上下文,具体结构如下
struct constexpr_ctx {
/* 当前的全局constexpr上下文 */
constexpr_global_ctx *global;
/* 正在求值的constexpr函数 */
constexpr_call *call;
/* 保存当前循环的表达式,如果没有在循环中则为 NULL */
vec<tree> *save_exprs;
/* 当前constexpr对象的构造函数 */
tree ctor;
/* 正在构造的constexpr对象. */
tree object;
...
};
用于编译期constexpr调用栈的上下文
// 调用栈
static vec<tree> call_stack;
// 栈标记
static int call_stack_tick;
// 最近出现调用错误的 call_stack_tick
static int last_cx_error_tick;
管理调用栈的函数
/* 检查当前调用栈是否超过最深限制。如果没有则将调用压入栈 */
static int
push_cx_call_context (tree call)
{
++call_stack_tick;
if (!EXPR_HAS_LOCATION (call))
SET_EXPR_LOCATION (call, input_location);
call_stack.safe_push (call);
int len = call_stack.length ();
if (len > max_constexpr_depth)
return false;
return len;
}
/* 出栈 */
static void
pop_cx_call_context (void)
{
++call_stack_tick;
call_stack.pop ();
}
/* 调用出错,清空调用栈 */
vec<tree>
cx_error_context (void)
{
vec<tree> r = vNULL;
if (call_stack_tick != last_cx_error_tick
&& !call_stack.is_empty ())
r = call_stack;
last_cx_error_tick = call_stack_tick;
return r;
}
通过这些数据结构我们已经可以粗略地得到一些结论。
在全局constexpr上下文里保存整个调用栈的局部变量和临时变量,保存在计算最外层常量表达式时创建的堆。
constexpr上下文保存的则是当前函数调用栈相关的对象,如当前调用的函数,当前循环的表达式变量,当前的所处的构造函数,正在构造的constexpr对象等。
编译器维护了一个编译期的函数调用栈,用来模拟函数调用过程,以便编译期计算。
现在来看更具体一点的计算过程。
下面这个函数是编译期计算的入口,它尝试将表达式 T
推导计算为常量。推导失败时,返回错误标记节点。
static tree cxx_eval_constant_expression (const constexpr_ctx *ctx, tree t,
bool lval,
bool *non_constant_p, bool *overflow_p,
tree *jump_target) {
// 在这个函数里有一大堆switch来判断当前表达式的类型,
// 将推导转移到下面的cxx_eval开头对应类型的函数
switch (TREE_CODE (t))
{
case BIND_EXPR:
case STATEMENT_LIST:
case LOOP_EXPR:
case COND_EXPR:
...
}
...
}
以下函数则是真正执行计算的函数,我挑选了几个比较常见的进行注释。
static tree cxx_eval_array_reference (...);
static tree cxx_eval_bare_aggregate (...);
// 计算二元表达式
static tree cxx_eval_binary_expression (...);
static tree cxx_eval_bit_cast (...);
static tree cxx_eval_bit_field_ref (...);
static tree cxx_eval_builtin_function_call (...);
// 对constexpr函数表达式进行编译期求值
static tree cxx_eval_call_expression (...);
static tree cxx_eval_check_shift_p (...);
static tree cxx_eval_component_reference (...);
// constexpr if表达式的实现,编译期计算条件表达式,不满足的分支将被抛弃
static tree cxx_eval_conditional_expression (...);
static tree cxx_eval_dynamic_cast_fn (...);
// 计算前置++/--表达式
static tree cxx_eval_increment_expression (...);
static tree cxx_eval_indirect_ref (...);
// 计算内置函数
static tree cxx_eval_internal_function (...);
// 计算逻辑表达式
static tree cxx_eval_logical_expression (...);
// 计算constexpr函数内的循环
static tree cxx_eval_loop_expr (...);
static tree cxx_eval_outermost_constant_expr (...);
static tree cxx_eval_statement_list (...);
static tree cxx_eval_store_expression (...);
// 计算switch分支
static tree cxx_eval_switch_expr (...);
static tree cxx_eval_thunk_call (...);
// 计算三元表达式
static tree cxx_eval_trinary_expression (...);
// 计算一元表达式
static tree cxx_eval_unary_expression (...);
static tree cxx_eval_vector_conditional_expression (...);
static tree cxx_eval_vec_init (...);
最后来总结一下。一个constexpr
函数内的所有表达式都是常量表达式,分支循环也不例外,编译器通过cxx_eval_constant_expression
将常量表达式分发到对应的cxx_eval
系列函数进行编译期计算,编译器通过模拟调用栈进行表达式树调用,局部变量以及求值完的常量放在了constexpr_ctx
中,后续解析代码时遇到求值完的表达式直接将表达式替换为常量。
因笔者水平有限,请各位读者审慎斟酌,如有所疏漏,欢迎在评论区指正。