编译期计算,是 C++ 编写高效代码的一个重要途径。C++20 中新增的 consteval 和 constinit 可以看做 constexpr 常量表达式的进一步延伸,而对于 constexpr 本身,以及常量表达式的相关使用,C++20 也在不断进行优化和改进。
由于 constexpr 只是告诉编译器这个表达式可以在编译期计算,但并不像 consteval 那样强制只能在编译期计算,而是可以同时用于编译期计算和运行期计算,所以很多语法可以放宽出现在常量表达式中,只要能在编译期计算出结果,或者是编译期未被使用的分支。
允许在常量表达式中调用虚函数、使用 dynamic_cast 和 typeid
早期C++标准中规定,常量表达式不可以调用虚函数。粗看感觉没问题,虚函数本来就是运行期的动态绑定的,不应该在编译期的常量表达式中使用。但再深入想一下,常量表达式中调用函数的对象,本来就要求也是常量对象,对于常量对象,编译期有足够的信息知道这个对象时什么类型,应该绑定那个虚函数,因此在C++20中进行放宽,允许常量表达式中调用虚函数。
class CA
{
public:
virtual int func() const = 0; // <1> 虚函数接口本身可以不是constexpr,只要实际调用的是就行
int m_a { 3 };
};
class CB : public CA
{
public:
constexpr virtual int func() const { return m_a + 1; }
};
class CC : public CB
{
public:
virtual int func() const { return m_a + 2; } // <2> 继承树中间有不是constexpr也可以,只要不调用
};
class CD : public CC
{
public:
constexpr virtual int func() const { return m_a + 3; }
};
int main( int argc, char * argv[] )
{
constexpr int ( CA::* pf )() const = &CA::func; // <3> 指向类成员函数的指针
constexpr CB b1;
static_assert( b1.func() == 4 ); // <4> 编译期的常量表达式也可以调用虚函数
static_assert( (b1.*pf)() == 4 ); // <5> 通过指向类成员函数的指针来调用也可以,能正确绑定 CB::func
constexpr CC c1;
// static_assert( c1.func() == 5 ); // <6> CC::func 没有加 constexpr ,因此不能在常量表达式中使用
// static_assert( (c1.*pf)() == 5 );
constexpr CD d1;
static_assert( d1.func() == 6 ); // <7> 父类的虚函数不是constexpr也没影响,自身虚函数有constexpr就行
static_assert( (d1.*pf)() == 6 );
return 0;
}
和调用虚函数类似,C++20 标准也放宽了在常量表达式中对 dynamic_cast 和 typeid 的使用,具体从略。
允许在常量表达式中使用try/catch
try/catch 是 C++ 中的异常处理机制,异常一般是运行期才会出现的,因此 C++20 标准中进行了放宽,允许在常量表达式中使用 try/catch 语句,只要不产生异常就可以,也就是说不能执行 throw 语句,既然没有 throw ,那么 catch 部分的语句实际也是不会执行到的。
constexpr int func( int x )
{
try { // <1> C++17 不支持 try/catch 语句,C++20 放宽限制改为允许
return x + 1; // <2> 编译期计算的常量表达式,相关代码不能出现 throw
}
catch( ... ) {
return 0; // <3> 编译期计算的常量表达式,不会进入 catch 相关代码
}
return 0;
}
允许在常量表达式中改变联合体(union)中当前激活的元素
联合体(union)的成员变量是共享同一份内存的,因此同一时间只有其中一个成员变量是激活的,这时候如果去访问其他成员变量,则会出现未定义行为。
编译期计算不允许出现未定义行为,因此需要记录当前联合体中激活的元素,由于 C++ 的语法比较复杂,各种引用等的情况,所以早期 C++ 标准担心改变联合体中当前激活的元素,或造成其他引用等的地方的不同步,造成未定义行为,因此禁止常量表达式中改变联合体中当前激活的元素。
由于这个特性确实有必要使用,通过语法和具体实现的评估,确认是可以有效地跟踪联合体中当前激活的元素的变化,因此在 C++20 标准中,放开了这个限制,允许改变联合体中当前激活的元素。
union UA
{
int m_a;
float m_b;
};
constexpr int func()
{
union UA a1;
a1.m_b = 3.14; // <1> 首次设置联合体当前激活的元素
a1.m_a = 3; // <2> C++17 Error,不允许改变联合体中当前激活的元素,C++20 修正为正确
return a1.m_a + 2;
}
【往期回顾】