为什么不使用模板实现新内容?

Why isn't new implemented with template?

本文关键字:实现 新内容 为什么不      更新时间:2023-10-16

我试图用一些额外的信息来检测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与模板实现?这将允许开发人员获得有关被分配对象类型的信息。

谢谢

大多数关于为什么c++是如何设计的问题归结为两种可能性。 c++的设计和发展给出了一个原因,或者我们只能推测。我相信这属于后一类。

首先,operator new(全局的和每类的)的使用(和替换的能力)早于模板添加到语言中。因此,最初是不可能用模板完成的。库的一些部分被切换到以前没有的模板,但大多数情况下这样做的好处是非常明显的。

第二,在早期具有模板功能的编译器中使用模板可能会导致问题,特别是对于较大的程序。问题在于函数模板不是函数——可以说,它是创建函数的配方。换句话说,如果将其实现为模板,则每种类型的每个分配器都将导致实例化一个单独的函数。当前的编译器(和链接器)非常擅长事后合并,但早期的编译器大多不是这样。在一个大型系统中,你可以很容易地创建几十个甚至几百个独立的分配函数。

最后,模板通常仍然只是介于普通用户代码和库中的之间的中间层,它提供了至少与当前operator newoperator delete大致一致的东西,它们与操作系统(或硬件,或其他)交谈,以确定从哪个池中提取内存,并(通常)将块子分配给用户代码。在某个地方,您需要一段(通常相当可观的)代码来创建和管理内存池。如果在模板中直接处理这个问题,它将(在实践中)必须在头文件中实现,这也将导致编译时间,这在66 MHz的Pentium上可能是相当不可接受的,甚至在我们大多数人只能梦想的令人难以置信的300 MHz DEC Alpha机器上。

可以not重写全局操作符new。可以做的是提供一个替换——这发生在链接期间,而不是在编译期间。

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

因为new基本上是一个未定义的引用,你可以很容易地提供一个定义来代替它。(见下面的细节#1)

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

或者允许operator new被覆盖。然后编译器必须在编译期间选择正确的operator new。但这有一个缺点,即只有在其翻译单元中包含"新new"的代码才能看到并使用它。现在考虑这里发生了什么:

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

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

此外,您需要确保使用new分配的char *不会通过"标准"delete获得delete d。类特定的操作符

也存在类似的问题:
class Baz; // forward declaration
void freeThatBaz(Baz * b) { delete b; }

这实际上是调用类特定的操作符delete还是全局操作符,这是实现定义的(从c++ 17开始,这是IIRC之前未定义的行为)。

所以我想说:基本上,不值得在标准中制定如此庞大而复杂的规则,甚至不被认为是"良好实践"。

# 1

细节

编译以下代码:

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)

operator new的一个未定义引用,与类型无关。如果模板,则模板类型参数将是符号的一部分,因此您将有两个未定义的符号。