C++uninitialized_default_construct

2026-04-11 08:35:29 415阅读 0评论

uninitialized_default_construct:C++20里那个“不声不响就构造”的冷面杀手

你有没有试过,用 mallocoperator new 分配了一块原始内存,想让它“悄悄”变成一堆可用的对象,又不想挨个调用构造函数?或者在写自定义容器时,卡在“怎么让未初始化的内存真正活过来”这一步?——别急,C++20 给你递了一把薄刃小刀:std::uninitialized_default_construct

它不像 new T[n] 那样自带内存分配,也不像 std::construct_at 那样只管单个对象。它的任务很专一:在已知地址的原始内存上,对一段范围内的对象执行默认构造(不传参),且不做内存分配、不抛异常(对平凡类型)、不越界访问

听起来像“构造函数的 memcpy 版本”?不完全是。关键在于:它尊重类型的构造语义,哪怕只是空构造函数,也真·调用;而 memcpystd::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 是平凡类型(如 intstd::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::vectorstd::deque 已把这事干得滴水不漏。但它存在的价值,是让你看清容器底层的“呼吸节奏”:分配 → 构造 → 使用 → 析构 → 释放。少了哪一环,内存就成哑巴。

当你调试 core dump 发现某段 std::string* pp->size() 返回垃圾值,回头一看:p 所指内存只 malloc 了,没 construct_at,更没 uninitialized_default_construct——那一刻,你会真心感谢这个冷门算法。

它不炫技,不讨好,只在你需要亲手托起对象生命周期时,稳稳接住那一小片内存。
就像老木匠不用电锯,偏爱凿子——不是不懂效率,是知道有些边界,必须亲手刻。

文章版权声明:除非注明,否则均为Dark零点博客原创文章,转载或复制请以超链接形式并注明出处。

发表评论

快捷回复: 表情:
验证码
评论列表 (暂无评论,415人围观)

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

目录[+]