从gcc源码浅谈constexpr

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中,后续解析代码时遇到求值完的表达式直接将表达式替换为常量。

因笔者水平有限,请各位读者审慎斟酌,如有所疏漏,欢迎在评论区指正。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值