C++uninitialized_default_construct
uninitialized_default_construct:C++20里那个“不声不响就构造”的冷面杀手
你有没有试过,用 malloc 或 operator new 分配了一块原始内存,想让它“悄悄”变成一堆可用的对象,又不想挨个调用构造函数?或者在写自定义容器时,卡在“怎么让未初始化的内存真正活过来”这一步?——别急,C++20 给你递了一把薄刃小刀:std::uninitialized_default_construct。
它不像 new T[n] 那样自带内存分配,也不像 std::construct_at 那样只管单个对象。它的任务很专一:在已知地址的原始内存上,对一段范围内的对象执行默认构造(不传参),且不做内存分配、不抛异常(对平凡类型)、不越界访问。
听起来像“构造函数的 memcpy 版本”?不完全是。关键在于:它尊重类型的构造语义,哪怕只是空构造函数,也真·调用;而 memcpy 或 std::fill 连构造函数的边都摸不到。
先看最典型的使用场景:手写一个极简 vector。
template<typename T>
class tiny_vec {
T* data_ = nullptr;
size_t cap_ = 0;
size_t size_ = 0;
public:
void reserve(size_t n) {
if (n <= cap_) return;
auto new_data = static_cast<T*>(::operator new(n * sizeof(T)));
// 此时 new_data 指向的是 raw bytes —— 对象尚未存在
// 我们不能直接用 data_[i],更不能 for 循环调用 default ctor(可能抛异常且低效)
std::uninitialized_default_construct(new_data, new_data + n);
if (data_) {
std::destroy(data_, data_ + size_);
::operator delete(data_);
}
data_ = new_data;
cap_ = n;
}
void resize(size_t n) {
if (n > size_) {
// 只需构造新增部分,已有对象保持原样
std::uninitialized_default_construct(data_ + size_, data_ + n);
} else if (n < size_) {
std::destroy(data_ + n, data_ + size_);
}
size_ = n;
}
};
注意两个细节:
reserve里,我们分配内存后,立刻用uninitialized_default_construct把 [begin, end) 范围“激活”为合法对象;resize扩容时,只构造新增段,不碰已存在的对象——这是性能关键,也是手动管理内存时最容易踩的坑:误对已构造对象重复调用构造函数,UB 就在下一秒。
那它到底做了什么?拆开看:
- 对于
T是平凡类型(如int、std::array<char,16>),它什么也不做——因为默认构造即“不做事”,内存已是有效位模式; - 对于非平凡类型(如
std::string、自定义类含成员初始化器),它逐个调用T{},等价于::new(p) T{}; - 它内部会检查是否支持
is_trivially_default_constructible_v<T>,从而跳过无意义操作; - 它不保证异常安全下的回滚——如果第 5 个对象构造失败,前 4 个已构造的对象不会自动析构。你得自己 catch 并手动
std::destroy前缀段。
这点常被忽略:uninitialized_default_construct 是“尽力而为”,不是“兜底保障”。真实项目中,若 T 构造可能抛异常,建议配合 std::uninitialized_default_construct_n + 异常捕获 + 清理逻辑,或直接换用 std::vector。
再聊个容易混淆的点:它和 std::default_initializable 的关系。
后者是概念(concept),描述“T{} 是否语法合法且不删除”;前者是算法,依赖该概念成立才可调用。但满足 default_initializable 不代表构造零开销——比如 std::mutex 满足该 concept,但默认构造会初始化内核对象。uninitialized_default_construct 照样调用它,该花的系统资源一分不少。
所以别被名字骗了:“uninitialized” 指输入内存未初始化,“default_construct” 指行为是默认构造——合起来就是:我不管内存之前是啥,现在我要按标准方式把它变成一堆刚出生的对象。
最后说句实在话:你日常写业务代码,大概率用不上它。std::vector、std::deque 已把这事干得滴水不漏。但它存在的价值,是让你看清容器底层的“呼吸节奏”:分配 → 构造 → 使用 → 析构 → 释放。少了哪一环,内存就成哑巴。
当你调试 core dump 发现某段 std::string* p 的 p->size() 返回垃圾值,回头一看:p 所指内存只 malloc 了,没 construct_at,更没 uninitialized_default_construct——那一刻,你会真心感谢这个冷门算法。
它不炫技,不讨好,只在你需要亲手托起对象生命周期时,稳稳接住那一小片内存。
就像老木匠不用电锯,偏爱凿子——不是不懂效率,是知道有些边界,必须亲手刻。


还没有评论,来说两句吧...