相关内容参考:C++中constexpr 与 explicit关键字使用详解
1. constexpr ------"让编译器做更多事"
① 提出动机:提升性能,减少运行时开销
在 C++11 之前:
- 只有
const,但 const 不保证编译期求值 - 想要编译期常量,只能用
#define或enum------都不够安全、不支持函数、不支持复杂类型
因此提出 constexpr 解决两个痛点:
痛点1:需要真正的编译期常量(strong compile-time constant)
例如:
cpp
const int N = foo(10); // 不能保证 foo(10) 在编译期运行
int arr[N]; // 可能无法作为数组大小
C++ 希望:如果值能在编译期确定,就应该在编译期确定。
痛点2:需要编译期可执行的函数(constexpr function)
例如:
cpp
double area(double r) { return 3.14159 * r * r; }
constexpr double A = area(10); // C++03 之前不行
C++ 希望:
- 数学函数
- 小工具函数
- 初始化对象
- 甚至类构造函数
都能在编译期执行。
② constexpr 给 C++ 带来的核心能力
编译期执行函数(Compile-time evaluation)
极大提高运行效率 + 常量折叠 + 去掉运行时开销。
更强的类型安全
比 #define 可靠得多。
用于模板元编程(metaprogramming)
constexpr 让模板元编程更直观,不必写复杂的 TMP 结构体推导。
允许类的编译期构造
例如:
cpp
struct Vec3 {
double x, y, z;
constexpr Vec3(double x, double y, double z) : x(x), y(y), z(z) {}
};
③ 总结------constexpr 提出的根本动机
让编译器"成为运行时",能提前计算任何能计算的东西,提高性能与安全性,增强 C++ 的表达能力。
一句话:
constexpr = 让 C++ 从运行时语言 → 编译期可计算语言
2. explicit ------"拒绝隐式转换导致的灾难"
① 提出动机:防止隐式类型转换引发 bug
在 C++98 之前:
构造函数可以隐式调用,导致非常多的意外错误:
cpp
struct Vec {
Vec(int x) {} // 可隐式转换
};
Vec v = 5; // ??? 编译器会自动调用 Vec(5)
问题:
- 难以察觉的隐式转换
- 编译器默默干了不想要的事
- 操作符重载时尤其危险
如:
cpp
bool operator==(const Vec&, const Vec&);
Vec v;
bool t = (v == 10); // 会被隐式转成 Vec(10)
这是非常危险的行为。
② explicit 的目标:禁止不安全的隐式构造
cpp
explicit Vec(int x);
禁止:
cpp
Vec v = 5; // 不再允许
保留:
cpp
Vec v(5); // 明确构造
③ explicit 的更高层动机(C++11 之后)
C++11 支持:
explicit用于转换运算符explicit(false)/explicit(true)(C++20)
用于禁止 "过度智能" 的隐式行为。
例如:
cpp
explicit operator bool() const;
防止:
cpp
Vector v;
if(v) {} //
int x = v + 1; // 防止自动转 bool 再转 int 的怪行为
④ 总结------explicit 提出的根本动机
explicit 的目的就是阻止编译器做你没说过的隐式转换,避免隐式构造引发隐蔽 bug。
一句话:
explicit = 禁止偷偷摸摸的隐式转换,保护你不被语言陷阱坑死。
对比总结
| 关键字 | 核心动机 | 解决的问题 |
|---|---|---|
| constexpr | 让编译期可以计算更多东西,提高性能与安全性 | 编译期常量、constexpr 函数、constexpr 构造函数 |
| explicit | 禁止隐式转换,减少意料之外的 bug | 防止构造函数隐式调用,防止隐式转换 |
3 const vs constexpr 对比
C++ 中 const 和 constexpr 都与常量相关,但用途和能力差异非常大:
| 特性 | const | constexpr |
|---|---|---|
| 定义 | 声明一个值不可修改 | 声明一个值或函数可以在编译期求值 |
| 是否保证编译期 | 不保证,可能运行时初始化 | 保证,如果满足条件,必须在编译期计算 |
| 适用对象 | 变量、成员、函数返回值 | 变量、成员、函数、构造函数、类常量 |
| 初始化 | 可以运行时初始化 | 必须用编译期常量初始化(C++14 起 relax,可有简单运行时计算) |
| 函数 | const 函数限制成员修改(方法级) |
constexpr 函数在编译期求值,并且可用于常量表达式 |
| 数组长度 | const int N = 10; int arr[N]; 编译器可能支持,但不保证 |
constexpr int N = 10; int arr[N]; 必定可用于编译期大小 |
| 优化 | 运行时常量,编译器可优化 | 编译期常量,可完全消除运行时开销 |
举例
cpp
const int a = 5; // 运行时可变对象也可初始化为常量
int arr1[a]; // 在一些编译器中可行,但非标准保证
constexpr int b = 5; // 编译期常量
int arr2[b]; // 标准保证可用
函数对比
cpp
const int square_const(int x) { return x*x; } // 运行时求值
constexpr int square_constexpr(int x) { return x*x; } // 编译期求值可能
square_constexpr(3)→ 编译期可求值square_const(3)→ 编译期不保证,只能运行时求值
4 explicit 在模板类 / 复杂构造中的用法
explicit 最初是为了防止隐式类型转换,但在 模板类 / 泛型编程 中尤其重要:
① 模板类单参数构造
cpp
template<typename T>
struct Wrapper {
explicit Wrapper(T val) : value(val) {}
T value;
};
Wrapper<int> w1(5); // 明确构造
Wrapper<int> w2 = 5; // 错误,禁止隐式转换
在模板类中,如果不加
explicit,可能会导致意想不到的隐式类型转换,尤其当模板参数为复杂类型(比如矩阵、点云类型)时。
② 多参数模板构造 + 默认参数
cpp
template<typename T>
struct Point {
explicit Point(T x = 0, T y = 0, T z = 0) : x(x), y(y), z(z) {}
T x, y, z;
};
Point<int> p1(1,2,3); // 明确构造
Point<int> p2; // 默认参数构造
Point<int> p3 = {}; // 禁止隐式列表初始化
加
explicit可以避免模板类在 列表初始化 / 隐式转换 时产生不安全行为。
③ explicit 与转换运算符(C++11+)
C++11 引入了 explicit operator,用于模板类中的类型安全转换:
cpp
template<typename T>
struct Vec {
T x, y;
explicit operator bool() const { return x != 0 || y != 0; }
};
Vec<int> v{0,0};
if (v) { } // 允许
int n = v; // 禁止隐式转换
- 模板类中尤其重要 ,因为模板类型可能变化,不加
explicit很容易产生隐式转换错误。
④ 小结
-
const vs constexpr
const= 不可修改(可能运行时求值)constexpr= 编译期求值,可用于模板/数组/常量初始化
-
explicit 在模板类中
- 防止单参数模板构造产生隐式类型转换
- 避免列表初始化 / 默认参数带来的隐式调用
- 可用于转换运算符,增加类型安全性