文 / 李博 (光宇广贞)
const 关键字的缺陷及 constexpr 关键字的产生
话说现行标准 C++ 98/03 对于常表达式(Constant Expression)的界定过于严格。C++ 09 新标准由 Bjarne Stroustrup 等人提案(N2116)“普通化常表达式(Generalized Constant Expression)”和 constexpr 关键字来试图解决这一问题。
是什么问题呢?先举两个 const 约束失败的例子。如下两图所示:
第一个图演示了一种 const 约束失败的情况。尽管由于 Fck::sb 值在编译期(Compile Time)已知使得 a 的值可在编译期计算,然而由于 const int 型变量 a 在常静态域 Fck::sb 赋值之前赋值,使得编译器无法为 a 量值(Evaluation),导致对 const int a 的初始化被判定为动态初始化(Dynamic Initialization),不再符合常表达式的语义,因此报错。
第二个图演示了另一种 const 约束失败的情况。即便 Fck 函数是静态的,是内联的,而且返回常值,但这些都没用,用 Fck 函数初始化 const int b 的过程将被视为动态初始化。C++ 这一对“常值函数”使用的苛刻约束,使得 C++ 程序员不得不留恋于 C 风格的 #define MACRO。
问题说到这儿。由于 const 关键字用法受拘束的不佳表现,C++ 09 尝试引入 constexpr 关键字。尤其是针对 const 对函数的无能,提出“常表达式函数(Constant Expression Function)”新标准。下文将简称“常函数”。常函数要求编译器在编译期对其量值。常函数默认是内联(inline)的。常函数必须满足如下标准(647 号议题,2008.6):
- 非虚函数。
- 返回值和参数表都须为“字面类型(Literal Type)”。
- 函数体只能是 return expression; 的形式,expression 应为潜在的常数表达式。
- 函数体被声明为 constexpr。
- 由 expression 到返回类型的隐式转化(implicit conversion)须为常表达式允许的转化形式之一。
使用中也有要求,首先便是 return expr; 这个 expr 必须是编译期在参数代入后可量值为常数,再者,常函数未完整定义时不得调用。下面举一些常函数的例子:
constexpr 关键字也可以修饰变量,该变量必须由常表达式初始化,如常值、constexpr 常函数及二者各自或相互间的运算表达式。constexpr 变量与 const 变量的区别在于,前者使用前必须以 constexpr 常表达式初始化,或者也可以说,前者可以使用更灵活的 constexpr 常表达式初始化……
最后说说这个“字面类型(Literal Type)”,该概念由 Bjarne Stroustrup 提出,644 号议题(2008.2)将此概念定义如下:
A type is a literal type if it is:
- a scalar type; or
- a class type with
- a trivial copy constructor,
- a trivial destructor,
- a trivial default constructor or at least one constexpr constructor other than the copy constructor,
- all non-static data members and base classes of literal types; or
- an array of literal type.
这里又涉及到一个概念:trivial class……这一概念是与 C99 标准兼容的尝试,在此就不展开了,读者感兴趣的话自己查文档吧……
constexpr 关键字的问题
VS 2010 明确拒绝接受此关键字加入语言核心,IDE 根本就不认识 constexpr。当然,微软此举有自己的考虑。微软 VC 开发团队博客指出,类似 sizeof 在 339 号议题中的遭遇一样,开篇所提到的 GCE 的使用也会遇到其在编译期内模板参数类型推导中函数重载指定(Overload Resolution)的问题。正如 339 号议题开篇的那句话:
“I’ve seen some pieces of code recently that put complex expressions involving overload resolution inside sizeof operations in contant expressions in templates.”
2007 年 7 月多伦多会议对 339 号议题讨论记录说,该问题在于是否允许任意表达式(Arbitrary Expressions)(如包含函数重载绑定的表达式)出现在模板推导上下文中,并且如何区分 SFINAE 失败和编译错误。
为何搞得这么麻烦呢?原因在于现行大多数主流编译器都拒绝在模板推导中执行完全语义检查,而代之以 SFINAE 规则,就是编译器在检查时,若一时未遇到精确匹配的替代者并不立即报错,而尝试寻找其它比较合适的替代者,直至找不到。而完全语义检查对于编译器来说是一项“受累不讨好”的工作,因此鲜有编译器愿意去实现它。但是编译器的“偷工减料”却让标准制定左右为难,一方面想维护自己做为标准的理想的完美性,另一方面又怕太让编译器为难而“逼迫”编译器供应商对标准“自我仲裁”,架空标准。就好像 C99 目前的尴尬处境一般。
说回到刚才的问题,由于常表达式允许常函数存在,而常函数是具类型的,且可以重载,若其做为模板参数,对其检查便涉及到模板的类型推导过程。又由于类型推导使用 SFINAE 机制而非完全语义检查,报错是“迟钝”的——这往往会导致误报。因此,这些因素罗在一起,可能会让编译器感到很为难。
尽管 339 号议题最新(最新也截止到 2007.7 多伦多会议)通过的决议对 SFINAE 失败(failure)和错误(error)做了明确的界定,然而决议最后也明确指出编译器为此将付出的惨重代价。直至目前,该议题未获通过。
更糟的是,C++ 0x 标准委员会的态度是,未将 339 号议题 sizeof 的问题解决了,是不会去搭 GCE 的茬儿的……constexpr 是否真的要胎死腹中了?
问题不止于此,正如 647 号议题对常函数的定义细则涉及到的“字面类型”约束,使得 constexpr 关键字对于自定义类成员函数的修饰受到限制。669 号议题规定 constexpr 对类成员函数的约束必须使函数体定义放进类声明中,不许声明与定义分离。这无疑是对 C++ 语言优雅的一种破坏。而定义细则的第五条又涉及到编译期浮点表达式计算精度(652 号议题)和整型与浮点型转化(707 号议题)的问题。常函数的递归如何定义等等……
问题可能还有很多很多。
影响
由于 BS 等人提交的 Initializer lists(N1493 N1509 N1890 N1919 N2100) 方案需要 constexpr 的支持,因故该语言内核使用性上改进的新特性同样为 VS 2010 拒绝。
参考: