注:大前提,本篇文章是在介绍C++11中的constexpr,自C++14以来constexpr有了非常大的改动,如在实验中遇见与本文不符的地方还先请查阅其他资料,确定为本文错误后可留言,我会虚心接受并改正。
constexpr定义编译时常量
在C++11中添加了一个新的关键字constexpr,这个关键字是用来修饰常量表达式的。所谓常量表达式,指的就是由多个(≥1)常量(值不会改变)组成并且在编译过程中就得到计算结果的表达式。常量表达式和非常量表达式的计算时机不同,非常量表达式只能在程序运行阶段计算出结果,但是常量表达式的计算往往发生在程序的编译阶段,这可以极大提高程序的执行效率,因为表达式只需要在编译阶段计算一次,节省了每次程序运行时都需要计算一次的时间。在C++11中添加了constexpr关键字之后就可以在程序中使用它来修饰常量表达式,用来提高程序的执行效率。在使用中建议将 const 和 constexpr 的功能区分开,即凡是表达“只读”语义的场景都使用 const,表达“常量”语义的场景都使用 constexpr。
在定义常量时const和constexpr是等价的,都可以在程序的编译阶段计算出结果,并可以用来定义数组。
const int size_c = 1024;
constexpr int size_cpr = 2048;
int main()
{
int array1[size_c] = { 1,2,3 };
int array2[size_cpr] = { 3,4,5 };
return 0;
}
如果要定义一个结构体/类常量对象,可以这样写:
#include <iostream>
using namespace std;
template<class T>
struct Test1
{
T t;
int i;
array<double, 3> arr; // array在栈上存数据
};
struct Test2
{
string str;
vector<int> v;
};
int main()
{
constexpr Test1<char> t1{ 'a', 10, {1,2,3} }; // 必须要在定义时就赋值
cout << t1.t << " " << t1.i << endl;
int arr[t1.i] = {}; // 证实t1.i具有常量属性
//t1.arr[0] = 10; //error constexpr修饰的对象的成员也是常量,不能进行修改
//const/constexpr array<double, 3> a{ 5, 6, 7 };
//t1.arr = a;
//error 因为constexpr修饰的常量是在编译时确定的,而string、vector等容器的数据是存在堆上的,而程序对堆空间的管理是在运行时才开始的,所以会出现编译错误
//constexpr Test2 t2{ "123", {1,2,3} };
const Test2 t2{ "123", {1,2,3} }; // 这里的const不是声明t2为编译时常量(真常量),而是声明为运行时常量(只读属性)
//t2.str = "000"; //error 只读对象的成员也是只读的
return 0;
}
常量表达式函数
为了提高C++程序的执行效率,我们可以将程序中值不需要发生变化的变量定义为常量,也可以使用constexpr修饰函数的返回值,这种函数被称作常量表达式函数,这些函数主要包括以下几种:普通函数/类成员函数、类的构造函数、模板函数。
注:由于现在编译器版本都比较高,默认的使用的C++标准也比较高(大于C++11),相关源代码请基于 C++11 标准进行测试。
普通函数/类的成员函数
constexpr并不能修饰任意函数的返回值,使这些函数成为常量表达式函数,必须要满足以下几个条件:
函数必须要有返回值,return返回的表达式必须是常量表达式,并且整个函数的函数体内有且只能有一条return语句,不能出现除此之外的语句。(using、typedef、static_assert语句除外)(C++14对该规则有较大修改)
#include <iostream>
using namespace std;
// error 没有返回值
constexpr void test1()
{}
constexpr bool test2()
{
using my_type = int;
typedef int new_type;
static_assert(10 > 0, "10 not greater than 0");
return true;
}
constexpr int test3(int i)
{
return i > 0 ? i : 0;
}
int main()
{
test1();
test2();
int i;
cin >> i;
cout << test3(i) << endl;
return 0;
}
函数在使用之前,必须有对应的定义语句。
#include <iostream>
using namespace std;
constexpr int test();
int main()
{
// error 在定义前使用constexpr函数
constexpr int res = test();
cout << res << endl;
return 0;
}
constexpr int test()
{
return 1;
}
注:以上规则不仅对应普通函数适用,对应类的成员函数也是适用的。
构造函数
如果想用直接得到一个常量对象,也可以使用constexpr修饰一个构造函数,这样就可以得到一个常量构造函数了。常量构造函数有一个要求:构造函数的函数体必须为空,并且必须采用初始化列表的方式为各个成员赋值。
当我们在类的构造函数前加上constexpr关键字时,我们是在向编译器表明,只要传递给构造函数的参数都是constexpr,那么该构造函数就可以在编译时期执行,产生的对象也将是constexpr对象。这意味着,这样的对象可以在编译时期被初始化,并且可以在只允许使用constexpr的场合中使用。例如,constexpr对象可以用作常量,它们可以在编译时被计算和赋值给常量变量或枚举值。
#include <iostream>
using namespace std;
struct Test
{
public:
constexpr Test() : i(0), d(0.0), c('0')
{}
~Test() = default;
int i;
double d;
char c;
};
int main()
{
// 错误用法,这样使用构建出的对象不是常量
Test t1;
cout << t1.i << " " << t1.d << " " << t1.c << endl;
t1.i = 100;
cout << t1.i << " " << t1.d << " " << t1.c << endl;
Test t2;
t1 = t2;
cout << t1.i << " " << t1.d << " " << t1.c << endl;
// 正确用法
constexpr Test t3;
//t3.i = 100; // error
return 0;
}
模板函数
C++11 语法中,constexpr可以修饰函数模板,但由于模板中类型的不确定性,因此函数模板实例化后的模板函数是否符合常量表达式函数的要求也是不确定的。如果constexpr修饰的模板函数实例化结果不满足常量表达式函数的要求,则constexpr会被自动忽略,即该函数就等同于一个普通函数。
#include <iostream>
using namespace std;
struct Person
{
const char* name_;
uint16_t age_;
};
template<class T>
constexpr T dispatch(const T& t)
{
return t;
}
int main()
{
// 因为12345是字面量即常量,所以此时dispatch是常量表达式函数
constexpr int ret = dispatch(12345);
Person p1{ "zhangsan", 15 };
// 因为p1是变量,不是常量表达式,所以dispacth函数的constexpr失效
Person p2 = dispatch(p1);
cout << p2.name_ << " " << p2.age_ << endl;
constexpr Person p3{ "zhangsan", 15 };
// 因为p3为常量,所以此时dispacth函数的constexpr有效
constexpr Person p4 = dispatch(p3);
cout << p4.name_ << " " << p4.age_ << endl;
return 0;
}