一、constexpr 构造函数:编译期构造对象
constexpr 构造函数,说白了就是让对象能在编译期就构造出来。嗯,这个特性是 C++11 引入的,后来在 C++14 和 C++17 里又做了不少增强。我个人觉得,这是现代 C++ 里最实用的特性之一。
你想想看,如果对象能在编译期就创建好,那运行时就不需要再花时间构造了。这对性能敏感的场景特别有用。我在做游戏引擎的时候,就经常用这个特性来预生成一些配置对象。
1.1 什么是 constexpr 构造函数
constexpr 构造函数,就是被 constexpr 修饰的构造函数。它允许我们在编译期就创建对象。但这里有个前提——这个对象必须是「字面类型」。
什么叫字面类型?说白了就是能在编译期确定所有成员值的类型。比如整型、浮点型,或者所有成员都是字面类型的类。
核心要点:constexpr 构造函数 + 字面类型 = 编译期对象构造
1.2 基本语法
来看个最简单的例子:
class Point {
public:
// constexpr 构造函数
constexpr Point(double x, double y)
: m_x(x), m_y(y) {}
constexpr double getX() const { return m_x; }
constexpr double getY() const { return m_y; }
private:
double m_x;
double m_y;
};
// 编译期构造对象
constexpr Point origin(0.0, 0.0);
constexpr double x = origin.getX(); // 编译期就能拿到值
注意看,constexpr Point origin(0.0, 0.0) 这行代码,对象是在编译期创建的。运行时根本不需要执行任何构造代码。
1.3 C++11 的限制
我记得 C++11 刚出 constexpr 的时候,限制特别多。构造函数体必须是空的,所有初始化都只能在初始化列表里完成。而且成员函数也只能是 constexpr 的。
举个例子,C++11 里你不能这样写:
class Logger {
public:
constexpr Logger(const char* msg) {
// C++11 里不行!构造函数体不能有语句
// m_msg = msg;
}
};
嗯,这确实挺烦人的。很多稍微复杂点的逻辑都没法在编译期完成。
1.4 C++14 的放宽
C++14 放宽了不少限制。构造函数体里可以写一些简单的语句了,比如赋值、循环、条件判断。但要注意,这些语句必须能在编译期求值。
class Point {
public:
constexpr Point(double x, double y) {
// C++14 允许这样写了
m_x = x;
m_y = y;
}
constexpr void setX(double x) { m_x = x; }
constexpr void setY(double y) { m_y = y; }
private:
double m_x;
double m_y;
};
constexpr Point createOrigin() {
Point p(0.0, 0.0);
// 可以在编译期修改成员
p.setX(1.0);
return p;
}
我在项目中遇到过这样的情况:用 C++14 的 constexpr 构造函数来初始化一些配置表,省去了运行时解析配置文件的步骤,性能提升很明显。
1.5 C++17 的进一步改进
C++17 又往前走了一步。现在 constexpr 函数里可以用 if constexpr 做编译期条件判断,还能用 lambda 表达式。这让编译期编程变得更灵活了。
template<typename T>
class Wrapper {
public:
constexpr Wrapper(T value) : m_value(value) {}
constexpr T getValue() const {
if constexpr (std::is_pointer_v<T>) {
// 编译期就能判断类型
return *m_value;
} else {
return m_value;
}
}
private:
T m_value;
};
// 编译期就能确定行为
constexpr Wrapper<int> w1(42);
constexpr int val = w1.getValue(); // 42
1.6 字面类型的条件
一个类要成为字面类型,得满足几个条件:
| 条件 | 说明 |
|---|---|
| 至少有一个 constexpr 构造函数 | 可以是默认的,也可以是自定义的 |
| 所有非静态成员都是字面类型 | 包括基类和非静态成员变量 |
| 没有虚基类 | 虚继承会破坏编译期确定性 |
| 没有虚函数 | 虚函数需要运行时多态支持 |
| 析构函数不能是用户提供的 | C++14 之后放宽了,但仍有约束 |
注意:我曾经踩过一个坑——在 constexpr 构造函数里用了虚函数。编译直接报错。后来才意识到,虚函数表是运行时才能确定的,编译期根本没法处理。
1.7 实际应用场景
constexpr 构造函数最常见的用途就是创建编译期常量对象。比如:
class Matrix {
public:
constexpr Matrix(std::initializer_list<double> values)
: m_values(values) {}
constexpr double get(int row, int col) const {
return m_values[row * 4 + col];
}
private:
std::array<double, 16> m_values;
};
// 编译期创建变换矩阵
constexpr Matrix identity = {
1,0,0,0,
0,1,0,0,
0,0,1,0,
0,0,0,1
};
// 编译期就能计算
constexpr double element = identity.get(0, 0); // 1.0
嗯,这里要注意。不是所有对象都适合用 constexpr 构造。如果对象需要在运行时动态分配内存,或者依赖外部资源,那就不适合了。
1.8 性能考量
用 constexpr 构造函数最大的好处就是零运行时开销。对象在编译期就创建好了,直接放在程序的只读数据段里。运行时连构造函数都不用调用。
我建议在以下场景优先考虑 constexpr 构造函数:
- 配置数据、常量表
- 数学常量、变换矩阵
- 编译期就能确定的策略对象
- 模板元编程中的类型特征
小技巧:如果你不确定某个类能不能在编译期构造,可以用 static_assert 来验证:
static_assert(std::is_literal_type_v<MyClass>,
"MyClass 不是字面类型");
不过要注意,std::is_literal_type 在 C++17 里被标记为废弃了。更好的做法是直接尝试在 constexpr 上下文中使用它。
1.9 常见陷阱
我遇到过不少同事在 constexpr 构造函数上栽跟头。最常见的问题有:
- 构造函数体太复杂——编译期能做的事情有限,别想着在 constexpr 构造函数里做文件读写
- 忘了加 constexpr——成员函数也要加 constexpr,否则不能在编译期调用
- 用了非字面类型的成员——比如 std::string,它在堆上分配内存,不是字面类型
举个例子,这样写会编译失败:
class BadExample {
public:
constexpr BadExample(const std::string& name)
: m_name(name) {} // 错误!std::string 不是字面类型
private:
std::string m_name; // 堆分配,不是字面类型
};
那怎么办?可以用 const char* 或者 std::array<char, N> 来代替。
1.10 总结
constexpr 构造函数是现代 C++ 里一个很实用的特性。它让我们能把对象的构造提前到编译期,从而获得更好的运行时性能。从 C++11 到 C++17,这个特性一直在进化,限制越来越少,能力越来越强。
我个人觉得,掌握 constexpr 构造函数的关键在于理解「字面类型」这个概念。只要你的类满足字面类型的条件,就能在编译期构造对象。嗯,多写几个例子试试,很快就能上手。
一句话总结:constexpr 构造函数 = 编译期构造 + 零运行时开销 + 字面类型约束
公众号:蓝海资料掘金营,微信deep3321