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

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

本文关键字:是否 重载 运算符 一个 如果 new delete      更新时间:2023-10-16

C++参考页面列出了全局新运算符的8个类特定的重载。其中四个是为2017版本的C++添加的。

特定类别分配函数

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)

特定类别的职位分配功能

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)

该网站还列出了10个特定于类的版本的全局删除运算符,其中4个是在2017年推出的。

特定于类的通常解除分配函数

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)

特定类别的配售解除分配函数

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

如果我编写一个带有new和delete运算符的C++类,我是否需要重载所有运算符?我忽略了可替换的全局运算符,因为我只编写类特定的运算符。

另一个问题提供了关于编写符合ISO的新运算符和删除运算符的信息,但没有说明我是否应该重载所有运算符,或者只重载一些运算符。

这个关于类特定的new和delete运算符的问题的答案并没有说明是替换全部还是只替换其中的一部分。

如果你能提供C++标准的引文或C++记忆专家的评论,那将有所帮助。

不,您不需要为类编写所有新运算符和删除运算符的变体

与其他版本相比,更喜欢某些新版本和删除版本有多种原因。我将分别描述每一个原因。

与没有大小参数的操作符相比,几乎总是更喜欢有大小参数的delete操作符

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

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)

并故意省略或CCD_ 1这些版本。

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参数告诉我对象的大小或数组的大小。在编写基类时,我不知道派生类对象的大小,所以使用size参数会有所帮助。我的一些内存处理程序按大小隔离对象(当所有块大小相同时,更容易集中内存)。我可以使用size参数来快速选择要搜索的内存池,而不是搜索所有内存池。这将O(n)算法转化为O(1)动作。

我的一些内存分配器使用"链模型"而不是"块模型",size参数也有助于删除那里的内容。(如果内存分配器预先分配了一个巨大的块,然后像数组一样将该块分割成单独的块,我将其称为"块模型"。如果每个块都指向前一个和下一个块,比如链表或链,我将内存处理程序称为"链模型"。)因此,当有人从内存块链中删除一个块时,我想让delete操作符知道被删除的区块大小是正确的。我可以在断言的删除操作中放入一个断言(size==下一个区块的地址-这个区块的地址)。

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

既然C++17为新操作符提供了一个对齐参数,那么如果需要的话就使用它们。如果您需要性能,请在4、8或16字节的边界上对齐对象,这样做!它使程序更快一点。

假设您有一个可识别对齐的内存分配器。它知道一些对象最好存储在4字节边界上,因为这些对象很小,如果使用4字节边界,可以将更多的对象压缩到内存中。它还知道一些对象最好在8字节的边界上对齐,因为这些对象经常被使用。

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

2017 C++标准说:

当分配对齐度超过STDCP_DEFAULT_NEW_alignment的对象和对象数组时,重载解析执行两次:首先是对对齐感知函数签名,然后是对对齐感知功能签名。这意味着,如果具有扩展对齐的类具有不知道对齐的类特定分配函数,则将调用该函数,而不是全局对齐的分配函数。这是有意的:类成员应该最清楚如何处理该类。

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

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

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... );

使用类特定的放置新操作符来提供提示

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

这些是类特定的放置新操作符。

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 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;
}

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

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

如果我用new和delete运算符编写C++类,我需要重载所有这些吗?

不,您不需要重载所有它们。至少,您需要重载需要自定义的运算符。

我认为我们可以假设你在过载的运营商中做一些特定的事情,否则你无论如何都不需要它们。

问题变得越来越严重了我应该超载所有这些吗

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

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

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

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

C++运算符有"集合"、算术、流插入和提取、关系运算符等。通常的做法是,当集合中的一个运算符过载时,其他运算符也过载。

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