为什么不用模板实现新的?

时间:2016-09-14 17:25:00

标签: c++ c++11 memory-management

我正在尝试使用一些附加信息来new,以便追踪内存泄漏。我知道,我可以全局覆盖new运算符,但我惊讶地发现我无法检索有关所分配对象类型的任何信息(如果我错了,请纠正我)。显然,当您决定覆盖new运算符时,获取类型信息会很有用。

例如,我使用可变参数模板实现了newdelete的简单通用版本。

std::string to_readable_name(const char * str)
{
    int status;
    char *demangled_name = abi::__cxa_demangle(str, NULL, NULL, &status);
    if(status == 0) {
        std::string name(demangled_name);
        std::free(demangled_name);
        return name;
    }
    return "Unknown";
}

template <typename T, typename... Args>
T * generic_new(Args&&... args) throw (std::bad_alloc)
{
    const std::size_t size = sizeof(T);
    std::cout << "Allocating " << size << " bytes for " << to_readable_name(typeid(T).name()) << std::endl;
    return new T(std::forward<Args>(args)...);
};

template<typename T>
void generic_delete(T* ptr)
{
    const std::size_t size = sizeof(T);
    std::cout << "Deleting " << size << " bytes for " << to_readable_name(typeid(T).name()) << std::endl;
    delete ptr;
}

int main()
{
    auto i = generic_new<int>(0);
    std::cout << *i << std::endl;
    generic_delete(i);

    return 0;
}

我的问题是为什么new没有用模板实现?这将允许开发人员获得有关正在分配的对象类型的信息。

谢谢

2 个答案:

答案 0 :(得分:5)

关于为什么设计C ++的大多数问题都归结为两种可能性。 C ++的设计和演变给出了一个理由,或者我们只能推测。我相信这属于后一类。

首先,使用(以及替换)operator new(全局和每类)的功能早于向该语言添加模板。因此,最初可能无法使用模板。图书馆的某些部分被转换为以前没有的模板,但大多数情况下,这样做的好处非常明显。

其次,使用具有早期模板功能的编译器的模板可能会导致问题,尤其是对于较大的程序。问题是功能模板不是一个功能 - 它可以说是创建功能的秘诀。换句话说,如果它是作为模板实现的,那么每种类型的每个分配器都会导致实例化一个单独的函数。目前的编译器(和链接器)在合并后很好,但早期的编译器大多数都不是。在大型系统中,您可以轻松创建数十个甚至数百个单独的分配函数。

最后,模板通常仍然只是普通用户代码和库中某些之间的中间级别,提供至少与当前operator new和{最近大致一致的内容“。 {1}}与操作系统(或硬件或其他)交谈,以确定从哪个池中抽取内存,并(通常)将片段分配给用户代码。在某个地方,您需要一段(通常相当大的)代码来创建和管理内存池。如果这是直接在模板中处理的话,它(实际上)必须在头文件中实现,这也会导致编译时间在66 MHz奔腾甚至是令人难以置信的快速300 MHz DEC上可能是相当不可接受的。 Alpha机器我们大多数人都只能梦想。

答案 1 :(得分:4)

You can not override the global operator new. 可以做的是提供替换 - 在链接期间而不是在编译期间发生。

  

我的问题是为什么没有新的模板实现?

由于new基本上是一个未定义的引用,因此您可以轻松提供单个定义来替换它。 (见下面的细节#1)

现在如果new是模板化函数会怎么样?对于每个模板实例,都会有一个不同的未定义符号,您需要为其提供替换。您也可以将您的定义作为模板,但谁会实例化它?您可能需要为此更改名称解析规则(基本上是模板的工作方式)。

或者您反而允许operator new覆盖。然后编译器必须在编译期间选择正确的operator new。但这样做的缺点是,只有具有您的&#34;新new&#34;在其翻译单位可以查看和使用它。现在考虑一下这里发生了什么:

#include <memory>
// code to override new and delete
void bar(void) {
  std::unique_ptr<int> x = new int{};
}

以上对new的调用使用了您的代码,但是对delete的调用深埋在标准库的内部呢?该调用也需要查看您的代码。这反过来意味着标准库代码必须在您的翻译单元中,或者换句话说:它需要在头文件中实现。

此外,您需要确保使用char *分配的 - { - 1}}并未通过&#获得new d 34;标准&#34; delete。类特定运算符已存在类似问题:

delete

这实际上是调用特定于类的运算符class Baz; // forward declaration void freeThatBaz(Baz * b) { delete b; } 还是全局调用实现定义(因为C ++ 17,它在IIRC之前是未定义的行为)。

所以我要说:基本上不值得努力在标准中制定如此庞大而复杂的规则,而这些规则甚至没有考虑过#34;良好做法&#34 ;

细节#1

编译以下代码:

delete

导致

void foo(void) {
  auto x = new int{};
  auto y = new bool{};
  delete x;
  delete y;
}

[nix-shell:/tmp/wtf]$ nm -C new.o U _GLOBAL_OFFSET_TABLE_ 0000000000000000 T foo() U operator delete(void*, unsigned long) U operator new(unsigned long) 的单个未定义引用,与类型无关。如果 是模板,则模板类型参数将成为符号的一部分,因此您有2个未定义的符号。