【问题标题】:If I write operators new and delete for a class, do I have to write all of their overloads?如果我为一个类编写运算符 new 和 delete,我是否必须编写它们的所有重载?
【发布时间】:2018-01-14 03:21:29
【问题描述】:

C++ 参考页面列出了 global new operators 的 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)

该网站还列出了 global delete operators 的 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++ 类,我需要重载 all 吗?我忽略了可替换的全局运算符,因为我只编写特定于类的运算符。

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

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

如果您可以提供来自 C++ 标准或 C++ 内存专家的 cmets 的引用,那将有所帮助。

【问题讨论】:

  • 在我看来,this answer 拥有您正在寻找的信息。
  • 好吧,如果你想使用自定义分配方案,我认为重载所有执行实际分配和释放的运算符是谨慎的。那个,或者明确删除那些你不想支持的。
  • 您可以使用template 来定义位置newdelete。这将允许编译器生成类型专用代码。
  • @CodyGray 谢谢,Cody,但这确实回答了我的问题。它说 why 编写新的运算符,而不是 which 重载。它说重载特定类以提高性能或减少碎片。它没有说明是否写一些就足够了,或者是否所有都是必要的。

标签: c++ memory-management new-operator delete-operator


【解决方案1】:

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

有多种原因比其他版本更喜欢 new 和 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)

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

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

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

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

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

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

2017 C++ 标准说:

在分配对齐超过STDCPP_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... );

使用特定于类的placement-new 运算符来提供提示。

假设您编写了一个分配多个数据成员的类,并且您希望所有这些数据成员位于同一内存页上。如果数据分布在多个内存页面上,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;
}

【讨论】:

  • 谢谢。迄今为止最好的答案。非常详细易懂。
【解决方案2】:

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

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

我认为我们可以假设您在重载运算符中执行特定操作,否则您无论如何都不需要它们。

问题变得更加我应该超载所有这些吗?

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

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

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

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

The C++ operators 出现在“集合”、算术、流插入和提取、关系等方面。通常的做法是,当集合中的一个运算符重载时,其他运算符也是如此。

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

【讨论】:

  • 除了将它们设为私有之外,还希望完全删除它们。
【解决方案3】:

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

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

【讨论】:

    猜你喜欢
    • 2019-02-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-11-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多