目录
1.背景
union,人皆知而鲜用的一个类。我最近在阅读C++标准库的源码时,看到了如下实现:
cpp
template<typename T>
struct constant_init {
union {
T obj;
};
constexpr constant_init() : obj() {}
~constant_init() { /* do nothing, union object is not destroyed */}
};
constant_init 用来在编译期创建一个类型 T 的对象 obj,obj 就放在了匿名的 union 中。为何要把一个对象放在 union 中?只因不想让该对象被销毁。
union 便有此用,标准描述为:
Absent default member initializers, if any non-static data member of a union has a non-trivial default constructor, copy constructor, move constructor, copy assignment operator, move assignment operator, or destructor, the corresponding member function of the union will be implicitly deleted unless it is user-provided.
在没有默认成员初始化器的情况下,如果 union 的任一非静态数据成员具有非平凡的默认构造函数、拷贝构造函数、移动构造函数、拷贝赋值运行符、移动赋值运算符或析构函数,则其相应的成员函数将被隐式删除(除非用户显式定义)。
例如:
cpp
union S {
int a;
std::string str;
};
S 无法使用,因为 std::string 导致它的对应成员函数被隐式删除了。
可以通过以下方式来解决。
1.使用 default member initializer
cpp
union S {
int a;
std::string str = "union str"; // default member initializer
~S() {} // explicitly define a destructor
} s;
std::cout << s.str;
default member initializer 会构造一个 std::string 对象,于是成员 str 处于激活状态。此时,S 的默认构造函数不会被删除,但析构函数依旧被移除,需要显式写出(但 std::string 依旧不会被析构,必须显式调用析构)。
2.聚合初始化
cpp
union S {
std::string str;
int a;
~S() {} // explicitly define a destructor
} s = { "union str"s };
std::cout << s.str;
此时,S 的构造函数也被删除了。但是,聚合初始化能够初始化构造函数被删除的对象,聚合初始化的类本身就不允许有构造函数。
需要注意,union 的聚合初始化,默认初始化第一个成员,所以 str 被挪到最前面了。
3.显式写出被删除的函数
这个就不必给例子了。
2.核心作用与设计意图
该模板结构体是 C++ 编译期常量初始化的包装器 ,核心目标是让任意类型 T 支持 constexpr(编译期)默认初始化,同时通过 union 特性避免自动调用 T 的析构函数,适用于需编译期构造且手动管理生命周期的场景。
3.关键设计
union的妙用 :仅包含单个成员T obj,利用union的特性 ------ 编译器不会自动调用成员的析构函数(因union无法确定活跃成员),因此析构函数~constant_init()空实现是合理的,避免误调用obj的析构。constexpr构造函数 :C++11 及以上支持constexpr构造函数,obj()是T的值初始化(零初始化 + 默认初始化),确保编译期可构造(需T的默认构造满足constexpr要求或为平凡构造)。- 内存布局 :因
union仅一个成员,constant_init<T>的大小、对齐方式与T完全一致,无额外内存开销。
4.适用场景与代码示例
1.适用条件(必须同时满足)
T是 平凡析构类型 (std::is_trivially_destructible_v<T>为true),否则会导致内存泄漏(obj的析构未被调用)。T需支持编译期默认初始化(要么是内置类型 / POD,要么自定义constexpr T())。
2.典型用法
cpp
#include <type_traits>
// 原结构体(添加静态断言增强安全性)
template<typename T>
struct constant_init {
static_assert(std::is_trivially_destructible_v<T>,
"T must be trivially destructible to avoid memory leak");
union {
T obj;
};
// C++14+ 支持 constexpr 构造函数初始化列表
constexpr constant_init() : obj() {}
// 禁止隐式析构 union 成员
~constant_init() {}
};
// 示例1:内置类型(编译期初始化)
constexpr constant_init<int> ci_int; // ci_int.obj = 0(零初始化)
static_assert(ci_int.obj == 0, "compile-time check");
// 示例2:自定义 POD 类型
struct POD { int x; double y; };
constexpr constant_init<POD> ci_pod; // ci_pod.obj.x=0, ci_pod.obj.y=0.0
// 示例3:constexpr 自定义类型
struct ConstExprType {
constexpr ConstExprType() : val(42) {}
int val;
};
constexpr constant_init<ConstExprType> ci_cexpr;
static_assert(ci_cexpr.obj.val == 42, "compile-time check");
5.潜在风险与限制
1.非平凡析构类型禁用
若 T 是非平凡析构(如 std::string、std::vector),使用该结构体将导致内存泄漏(obj 的析构函数未被调用):
cpp
// 错误示例:std::string 是非平凡析构
constant_init<std::string> ci_str; // 构造时初始化空字符串(运行时)
// 析构时未调用 std::string 的析构,内存泄漏
2.constexpr 失效场景
若 T 的默认构造不是 constexpr(如包含动态内存分配),constant_init 的构造函数将退化为运行时调用,失去编译期初始化优势。
3.复制 / 移动语义缺失
默认生成的复制构造 / 赋值运算符会浅拷贝 obj,若 T 需深拷贝(如自定义带指针的类型),需手动实现拷贝逻辑,否则会导致浅拷贝问题。
6.注意事项
1.添加静态断言(如上文示例),强制检查 T 是平凡析构类型,避免误用。
2.手动实现复制 / 移动语义(若 T 需深拷贝):
cpp
constexpr constant_init(const constant_init& other) : obj(other.obj) {}
constant_init& operator=(const constant_init& other) {
obj = other.obj;
return *this;
}
3.若需支持 C++20+,直接使用 std::optional 替代,无需自定义结构体。