如果我为 class 编写运算符 new 和 delete,我是否必须编写它们的所有重载?

If I write operators new and delete for a class, do I have to write all of their overloads?

C++ 参考页列出了 global new operators 的 8 个 class 特定 重载。其中四个是为 2017 版 C++ 添加的。

Class-特定分配函数

void* T::operator new  ( std::size_t count );   
void* T::operator new[]( std::size_t count );
void* T::operator new  ( std::size_t count, std::align_val_t al ); // (since C++17)
void* T::operator new[]( std::size_t count, std::align_val_t al ); // (since C++17)

Class-特定的展示位置分配函数

void* T::operator new  ( std::size_t count, user-defined-args... );
void* T::operator new[]( std::size_t count, user-defined-args... );
void* T::operator new  ( std::size_t count,
    std::align_val_t al, user-defined-args... ); // (since C++17)
void* T::operator new[]( std::size_t count,
     std::align_val_t al, user-defined-args... ); // (since C++17)

该网站还列出了 global delete operators 的 10 个 class 特定 版本,其中 4 个是在 2017 年引入的。

Class-特定的常用释放函数

void T::operator delete  ( void* ptr );
void T::operator delete[]( void* ptr );
void T::operator delete  ( void* ptr, std::align_val_t al ); // (since C++17)
void T::operator delete[]( void* ptr, std::align_val_t al ); // (since C++17)
void T::operator delete  ( void* ptr, std::size_t sz );
void T::operator delete[]( void* ptr, std::size_t sz );
void T::operator delete  ( void* ptr, std::size_t sz, std::align_val_t al ); // (since C++17)
void T::operator delete[]( void* ptr, std::size_t sz, std::align_val_t al ); // (since C++17)

Class-特定的放置释放函数

void T::operator delete  ( void* ptr, args... );
void T::operator delete[]( void* ptr, args... );

如果我用 new 和 delete 运算符编写 C++ class,是否需要重载其中的 all?我忽略了可替换的全局运算符,因为我只写了 class 个特定的运算符。

This other question provides info on writing ISO compliant new and delete operators,但没有说明我是否应该重载所有这些,还是只重载一些。

this question about class specific new and delete operators 的答案没有说明是替换全部还是只替换其中的一部分。

如果您能提供 C++ 标准的引文或 C++ 内存专家的评论,那将会有所帮助。

您只需要重载您使用的 newdelete 版本。根据 [class.free 中的示例,在 class 中定义一个 operator new 函数将隐藏所有全局 operator new 函数。这与定义与基函数同名的方法相同 class 函数或全局函数隐藏基或全局版本。

请注意 operator newoperator new[] 是不同的名称,因此单独重载 operator new 不会隐藏全局 operator new[] 函数。

If I write a C++ class with new and delete operators, do I need to overload all of those?

不,您不需要让所有这些都超载。至少,您需要重载您需要自定义的运算符。

我想我们可以假设您正在重载运算符中做一些特定的事情,否则您无论如何都不需要它们。

问题变得更多我应该重载所有这些吗?

是的,您可能应该这样做。如果代码根据代码中使用的 newdelete 的形式做了完全不同的事情,那将是令人惊讶的,例如

auto* obj1 = new Obj{};
// vs
auto* obj2 = new Obj[5];

如果 new 运算符应用了一些特殊的初始化,则可以合理地预期两种形式都会进行该初始化。

另一方面是,如果其他形式不适用,则倾向于完全删除 (= delete) 这些重载。

The C++ operators 出现在 "sets"、算术、流插入和提取、关系等。通常的做法是,当一个运算符中的一个运算符被重载时,其他运算符也被重载。

它并不总是适用,但通常适用。例如。串联操作通常有 operator+operator+=,但没有 operator-operator-=

不,您不需要为 class.

编写新的和删除运算符的所有变体

有多种原因使某些版本的 new 和 delete 优于其他版本。我会分别描述每个原因。

几乎总是喜欢有大小参数的删除运算符而不是没有大小参数的删除运算符。

当我为为其他 class 提供内存处理的基 class 编写删除运算符时,我使用这些版本的删除运算符

void T::operator delete  ( void* ptr, std::size_t sz );
void T::operator delete[]( void* ptr, std::size_t sz );
void T::operator delete  ( void* ptr, std::size_t sz, std::align_val_t al ); // (since C++17)
void T::operator delete[]( void* ptr, std::size_t sz, std::align_val_t al ); // (since C++17)

并故意省略或 =delete 这些版本。

void T::operator delete  ( void* ptr );
void T::operator delete[]( void* ptr );
void T::operator delete  ( void* ptr, std::align_val_t al ); // (since C++17)
void T::operator delete[]( void* ptr, std::align_val_t al ); // (since C++17)

原因是std::size_t sz参数告诉我对象的大小或数组的大小。当我写我的基础 class 时,我不知道派生的 class' 对象的大小,所以使用大小参数有帮助。我的一些内存处理程序按大小分隔对象(当所有块的大小相同时更容易合并内存)。我可以使用大小参数来快速选择要搜索的内存池,而不是搜索所有内存池。这将 O(n) 算法变成了 O(1) 操作。

我的一些内存分配器使用 "chain model" 而不是 "block model",并且大小参数也有助于删除那里。 (我称内存分配器为 "block model",如果它预先分配了一个巨大的块,然后将块分成单独的块,如数组。如果每个块指向前一个和下一个块,我称内存处理程序为 "chain model"像链表或链。)所以当有人从内存块链中删除一个块时,我希望删除操作符知道被删除的块是正确的大小。我可以在断言的删除操作中放置一个断言(大小==下一个块的地址-该块的地址)。

在适当的情况下,首选带有对齐参数的 new 和 delete 运算符。

既然 C++17 为新的运算符提供了对齐参数,请在需要时使用它们。如果您需要性能,请在 4、8 或 16 字节边界上对齐您的对象,这样做吧!它使程序更快一些。

假设您有一个对齐感知内存分配器。它知道某些对象最好存储在 4 字节边界上,因为这些对象很小,如果使用 4 字节边界,您可以将更多对象挤入内存。它还知道某些对象最适合在 8 字节边界上对齐,因为这些对象经常使用。

如果您的内存处理程序提供了正确的新运算符并且派生的 classes 提供了正确的对齐值,您的内存处理程序就会知道这一点。

2017 C++ 标准说:

When allocating objects and arrays of objects whose alignment exceeds STDCPP_DEFAULT_NEW_ALIGNMENT, overload resolution is performed twice: first, for alignment-aware function signatures, then for alignment-unaware function signatures. This means that if a class with extended alignment has an alignment-unaware class-specific allocation function, it is the function that will be called, not the global alignment-aware allocation function. This is intentional: the class member is expected to know best how to handle that class.

这意味着编译器将检查带有对齐参数的 new 和 delete 运算符,然后检查没有对齐参数的运算符。

如果您有对齐感知内存处理程序,那么请始终提供这些新运算符,即使您还想为您的客户端代码提供忽略对齐的选项。

void* T::operator new  ( std::size_t count, std::align_val_t al ); // (since C++17)
void* T::operator new[]( std::size_t count, std::align_val_t al ); // (since C++17)
void* T::operator new  ( std::size_t count,
    std::align_val_t al, user-defined-args... ); // (since C++17)
void* T::operator new[]( std::size_t count,
     std::align_val_t al, user-defined-args... ); // (since C++17)

如果您提供上述新运算符并省略或=delete这些重载,则可以强制代码提供对齐参数。

void* T::operator new  ( std::size_t count );
void* T::operator new[]( std::size_t count );

void* T::operator new  ( std::size_t count, user-defined-args... );
void* T::operator new[]( std::size_t count, user-defined-args... );

使用 class 特定的 placement-new 运算符提供提示。

假设您编写了一个分配多个数据成员的 class,并且您希望所有这些数据成员都位于同一内存页上。如果数据分布在多个内存页面上,CPU 将不得不将不同的内存页面加载到 L1 或 L2 缓存中,以便您可以访问对象的成员数据。如果您的内存处理程序可以将一个对象的所有数据成员放在同一个页面上,那么您的程序将 运行 更快,因为 CPU 不需要将多个页面加载到缓存中。

这些是 class 特定展示位置的新运算符。

void* T::operator new  ( std::size_t count, user-defined-args... );
void* T::operator new[]( std::size_t count, user-defined-args... );
void* T::operator new  ( std::size_t count,
    std::align_val_t al, user-defined-args... ); // (since C++17)
void* T::operator new[]( std::size_t count,
     std::align_val_t al, user-defined-args... ); // (since C++17)

通过提供提示参数将它们重载为如下所示。

void* T::operator new  ( std::size_t count, void* hint );
void* T::operator new[]( std::size_t count, void* hint );
void* T::operator new  ( std::size_t count, std::align_val_t al, void* hint ); // (since C++17)
void* T::operator new[]( std::size_t count, std::align_val_t al, void* hint ); // (since C++17)

提示参数告诉内存处理程序尝试将对象放置在不在该提示地址的位置,而是与提示在同一页上地址.

现在您可以编写一个 class,它看起来像这样,它源自您的内存处理 class。

class Foo : public MemoryHandler
{
public:
    Foo();
    ...
private:
    Blah * b_;
    Wham * f_;
};

Foo::Foo() : b_( nullptr ), f_( nullptr )
{
    // This should put data members on the same memory page as this Foo object.
    b_ = new ( this ) Blah;
    f_ = new ( this ) Wham;
}