一、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 构造函数上栽跟头。最常见的问题有:

  1. 构造函数体太复杂——编译期能做的事情有限,别想着在 constexpr 构造函数里做文件读写
  2. 忘了加 constexpr——成员函数也要加 constexpr,否则不能在编译期调用
  3. 用了非字面类型的成员——比如 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 构造函数 = 编译期构造 + 零运行时开销 + 字面类型约束

constexpr 构造函数知识体系 constexpr 构造函数 语法演进 C++11:空函数体 C++14:放宽限制 C++17:if constexpr 字面类型条件 至少一个 constexpr 构造函数 所有成员都是字面类型 无虚基类 / 无虚函数 应用场景 编译期常量 / 配置数据 数学常量 / 变换矩阵 模板元编程类型特征 核心价值:编译期构造 → 零运行时开销 constexpr + 字面类型 = 编译期对象

公众号:蓝海资料掘金营,微信deep3321