C++uninitialized_move_n移动N未初

2026-04-11 07:55:34 914阅读 0评论

uninitialized_move_n:移动 N 个未初始化对象的“搬运工”,别再手写循环了

你有没有写过这样的代码:

T* dst = static_cast<T*>(::operator new(n * sizeof(T)));
for (size_t i = 0; i < n; ++i) {
    ::new (dst + i) T(std::move(src[i]));
}

或者更糟——直接用 std::move_iterator 配合 uninitialized_copy
结果发现类型不支持拷贝、只支持移动,而 uninitialized_copy 内部调用的是 *out++ = std::move(*in++)这根本不是 placement-new 构造,而是赋值。对象压根没在目标内存上“活”起来。

这时候,std::uninitialized_move_n 就不是“可选项”,而是你漏掉的那块拼图。


C++17 引入 uninitialized_move_n,它干一件事:在未初始化的原始内存上,逐个 placement-new 构造 T 类型对象,并把源范围的前 N 个元素以移动语义搬过去
注意关键词:未初始化内存placement-new 构造移动语义N 个(不是迭代器对)

它签名长这样:

template<class InputIt, class Size, class ForwardIt>
ForwardIt uninitialized_move_n(InputIt first, Size n, ForwardIt d_first);

返回值是 d_first + n,方便链式调用——这点很 C++,但容易被忽略。

为什么需要它?举个真实场景:
你正在写一个简易 vector<T>reserve 后扩容逻辑。旧数据在堆上,新内存刚 operator new 分配完,一片空白。你想把旧元素高效搬过去,且 T 是不可拷贝、仅可移动的(比如 std::unique_ptr<int> 或自定义资源句柄)。
std::uninitialized_copy 会失败(它调用 *out = std::move(*in),但 out 指向未构造内存,解引用即 UB);
std::move + std::copy 更不行——那是搬指针,不是构造对象;
手写循环?可以,但错一处(比如忘了 try-catch 回滚),就内存泄漏+析构未定义。

uninitialized_move_n 把这些兜底了:它内部对每个元素做 ::new (static_cast<void*>(d_first + i)) T(std::move(*(first + i))),并自动处理异常安全——只要某个构造抛异常,它会按逆序析构已成功构造的对象,然后重新抛出。


实用时有三个细节,文档常一笔带过,但踩坑率极高:

第一,目标内存必须足够大且未初始化
不是“未赋值”,是“完全没调用过任何构造函数”。用 mallocoperator new 分配的裸内存 OK;用 new T[n] 分配的?不行——那已经调用了 n 次默认构造,属于“已初始化”,此时该用 std::move + std::copy,而非 uninitialized_* 系列。

第二,nSize 类型,不是 size_t,且必须非负
-1 不会编译失败(Size 可能是 int),但行为未定义。实践中建议用 static_cast<size_t>(n) 显式转换,并前置校验 n >= 0——别指望库替你做运行时断言。

第三,它不负责释放源内存,也不修改源对象的状态
std::move 只是类型转换,真正“掏空”靠 T 自己的移动构造函数。如果 T 的移动构造没置空资源(比如忘了把 ptr_ = nullptr),后续对源对象操作仍可能 crash。这不是 uninitialized_move_n 的锅,但新手常误以为“用了 move 就万事大吉”。


怎么用才不翻车?给个最小可行示例:

#include <memory>
#include <iostream>

struct LogOnMove {
    int id;
    LogOnMove(int i) : id(i) { std::cout << "Ctor " << id << "\n"; }
    LogOnMove(LogOnMove&& rhs) noexcept : id(rhs.id) {
        std::cout << "Move-ctor " << id << " from " << rhs.id << "\n";
        rhs.id = -1; // 关键:置空,避免二次析构
    }
    ~LogOnMove() { if (id != -1) std::cout << "Dtor " << id << "\n"; }
};

int main() {
    LogOnMove src[3] = {1, 2, 3};
    void* raw = operator new(3 * sizeof(LogOnMove));

    auto last = std::uninitialized_move_n(
        src, 3,
        static_cast<LogOnMove*>(raw)
    );

    // 此时 [raw, raw+3) 已构造好,src[0..2] 处于有效但“掏空”状态
    // 记得显式析构目标区间
    for (auto p = static_cast<LogOnMove*>(raw); p != last; ++p) {
        p->~LogOnMove();
    }
    operator delete(raw);
}

输出清晰显示:移动构造发生,且 src 元素 id 被置为 -1,证明移动语义生效。
重点:uninitialized_move_n 只管构造,析构得你亲手 p->~T(),或封装进 RAII(比如 std::unique_ptr<T[], D> 自定义删除器)。


最后说句实在话:uninitialized_move_n 不是炫技工具。它存在的意义,是让底层容器、内存池、序列化框架这类代码,在面对仅可移动类型时,不用在安全和性能之间二选一
你不必再为“这段移动会不会漏析构”半夜改 bug,也不用为“要不要加 if constexpr(std::is_trivially_move_constructible_v<T>) 分支”纠结半天。

它安静,精准,不声张——就像所有真正好用的底层设施那样。
下次当你盯着一片 void* 和一个 T[] 源数组发呆时,试试它。
说不定,就省下半小时调试时间。

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

发表评论

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

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

目录[+]