在泛型编程中使用新的放置

Using placement new in generic programming

本文关键字:泛型编程      更新时间:2023-10-16

当在通用代码中使用放置新内容在指定地址构造对象时,使用模式与通常的代码略有不同。 例如,考虑uninitialized_copy的实现:([uninitialized.copy](

template <class It, class For>
For uninitialized_copy(It first, It last, For dest)
{
using T = typename std::iterator_traits<For>::value_type;
for (; first != last; ++first, (void)++dest)
::new (static_cast<void*>(std::addressof(*dest))) T(*first);
}

这篇文章从标准的角度解决了以下几点:

  • 为什么使用::new而不仅仅是new;

  • 为什么需要显式强制转换为void*

(此答案使用N4659,最终的C++17草案。

为什么使用::new而不仅仅是new

::new可确保在全局范围内查找operator new。 相反,如果纯new是类类型(或其数组(,则T首先在类的作用域中查找,然后才回退到全局作用域。 根据 [expr.new]/9:

如果new-表达式以一元运算符开头::则 分配函数的名称在全局范围内查找。 否则,如果分配的类型是类类型T或其数组, 分配函数的名称在T范围内查找。如果 此查找无法找到名称,或者如果分配的类型不是 类类型,分配函数的名称在全局中查找 范围。

例如,与

struct C {
void* operator new(std::size_t, void* ptr) noexcept
{
std::cout << "Hello placement new!n";
return ptr;
}
};

new将导致找到此函数,从而打印不需要的消息,而::new仍将找到全局函数并正常工作。

由于 [new.delete.placement]/1,无法替换全局operator new(std::size_t, void*)

这些函数是保留的;C++程序不能定义替换C++标准库中版本的函数。 ([约束](。[basic.stc.dynamic] 的规定不适用 到这些保留的operator newoperator delete的安置形式.

(有关重载operator new的详细信息,请参阅如何编写符合 ISO C++ 标准的自定义新建和删除运算符?

为什么需要显式强制转换为void*

尽管不能替换全局operator new(std::size_t, void*),但可以定义::operator new的新版本。 例如,假设以下声明放置在全局范围内:

void* operator new(std::size_t, int* ptr) noexcept
{
std::cout << "Hello placement new!n";
return ptr;
}

然后::new(ptr) T将使用此版本而不是全局版本,其中ptrint*值。 指针显式转换为void*,以确保operator newvoid*版本(我们打算称之为(在重载分辨率中获胜。


来自评论:

但是,如果某些人,我们为什么要为void*调用确切的全球new类型对自己有特殊的新过载吗?看起来很正常 重载运算符更合适 - 为什么不合适?

通常,new用于分配目的。 分配是用户应该控制的内容。 用户可以为普通new推出更合适的版本。

但是,在这种情况下,我们不想分配任何东西 — 我们要做的就是创建一个对象! 放置 new 更像是一种"黑客"——它的存在主要是由于缺乏可用于在指定地址构造对象的语法。 我们不希望用户能够自定义任何内容。 然而,语言本身并不关心这个黑客 - 我们必须特别对待它。 当然,如果我们有类似construct_at的东西(即将推出 C++20(,我们将使用它!

另请注意,std::uninitialized_copy适用于最简单的情况,即您只想在原始分配的空间中复制构造一系列对象。 标准容器不仅允许您通过分配器自定义元素的分配方式,还允许您自定义元素的构造方式。 因此,他们通常不对他们的元素使用std::uninitialized_copy——他们称之为std::allocator_traits<Allocator>::construct. 此功能由std::scoped_allocator_adaptor使用。