【问题标题】:Macro to replace C++ operator new替换 C++ 运算符 new 的宏
【发布时间】:2010-10-11 19:01:30
【问题描述】:

是否可以创建宏来将所有形式的operator new 替换为包含额外参数的重载...比如__FILE____LINE__

问题似乎在于operator new 既可以带括号也可以不带括号,因此:

  • 类对象宏:

    #define new new(__FILE__, __LINE__)
    

    将替换如下声明:

    A* a = new A();
    
  • 类函数宏:

    #define new(A) new (A, __FILE__, __LINE__)
    

    将替换如下声明:

    A* a = new(std::nothrow) A();
    

不幸的是,尝试用相同的标识符声明两个宏是错误的,即使它们是不同的类型,所以以下失败:

#define new new(__FILE__, __LINE__)
#define new(A) new (A, __FILE__, __LINE__) // Error: "new" already defined

由于我使用的是 g++,我希望使用他们的 variadic macros 语法会取得成功,但不幸的是没有。以下:

#define new(...) new(__FILE__, __LINE__, ## __VA_ARGS__)

只匹配new(xyx) A(),不匹配new A()

我知道essays have been written 知道为什么这是不可能的,但我觉得我已经很接近了,一定有办法。我有什么明显的遗漏吗?

【问题讨论】:

  • 请注意,尚不清楚#define new是否保证工作:open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#369
  • @JohannesSchaub-litb 但很明显,如果您在此之后还包含任何标准标头,则会调用 UB。
  • 不要不要在任何头文件中尝试#define 任何关键字或标准函数、类型、对象、模板等的名称:包括标准头文件,在你做了这样的#define 之后,有未定义的行为。

标签: c++ gcc c-preprocessor


【解决方案1】:

不,没有办法。

您可以在 malloc()/free() 的糟糕日子里这样做,但对于 new 却不行。

您可以通过全局覆盖new 运算符来替换内存分配器,但不能注入您所说的特殊变量。

【讨论】:

  • 你可以做到,我去年就做到了。没有什么棘手的。
  • @Robert:你能提供任何细节吗?
【解决方案2】:

您应该查看我的同事 Calvin 撰写的这篇出色的博客文章。我们最近遇到了一种情况,我们希望启用这种类型的修复,以便将内存泄漏与在诊断/调试构建中分配它们的行相关联。这是一个有趣的技巧

https://docs.microsoft.com/en-us/archive/blogs/calvin_hsia/overload-operator-new-to-detect-memory-leaks

【讨论】:

  • 该技术实际上并没有显示调用 new 的文件/行。当 FILELINE 作为默认函数参数传递时,您总是得到声明函数的文件/行。甚至他的示例输出也显示了这一点。
【解决方案3】:

您没有说您使用的是什么编译器,但至少使用 GCC,您可以覆盖 new 并记录调用者地址,然后使用 addr2line 将其转换为文件/行信息(或使用 BFD 库来做到这一点立即)。

【讨论】:

  • 糟糕,抱歉。您确实说过您正在使用g ++。所以你去。
  • 不幸的是 addr2line 需要调试符号才能工作。我们的可执行文件通常使用 -O2 而没有 -g 编译,所以我认为这行不通。如果我错了,请纠正我。
  • 您可以使用 -g 标志和 -O2 标志。此外,除非您指定 -s 标志,否则会有一些信息,例如地址属于哪个函数和哪个源文件(如果我没记错的话)。
  • 不幸的是 -O2 折叠(优化掉)一些函数调用,因此调用堆栈与源不匹配。
  • (已经有一段时间了,但以防万一......)您不必在部署的系统上使用 -g 。使用 -g 编译,如果您的部署需要,则使用“剥离”;在未剥离的“离线”版本上运行 addr2line;地址将相同。也适用于来自非 g 应用程序的 gdb 核心文件。
【解决方案4】:

3.7.4 动态存储时长

2 该库为全局分配和释放函数提供默认定义。一些全局分配和释放函数是可替换的 (18.5.1)。 C++ 程序应最多提供一个可替换分配或解除分配函数的定义。任何此类函数定义都会替换库中提供的默认版本 (17.6.4.6) [...]

17.6.4.6 替换函数

  1. C++ 程序可以提供八种动态内存分配中的任何一种的定义 标头中声明的函数签名(3.7.4,第 18 条):

    • 新的运算符(std::size_t)
    • 新运算符(std::size_t, const std::nothrow_t&)
    • 运算符 new[](std::size_t)
    • 运算符 new[](std::size_t, const std::nothrow_t&)
    • 运算符删除(void*)
    • 操作符删除(void*, const std::nothrow_t&)
    • 运算符删除[](void*)
    • 运算符 delete[](void*, const std::nothrow_t&)

希望这能澄清什么是合法的超载,什么不是。

这里的一些人可能会感兴趣:

#define delete cout <<  "delete called at: " << __LINE__ << " of " << __FILE__  << endl, delete 

using namespace std;

void *operator new(size_t size, ostream& o, char *f, unsigned l) {
    o << "new called at: " << l << " of " << f << endl;
    return ::new char[size];
}

int main() {
    int *a = new(cout, __FILE__, __LINE__) int;
    delete a;
}

Caveat Lector:我在这里做的是一件坏事 (TM) - 全局重载 new/delete。

【讨论】:

  • 为什么全局重载 new 是一件坏事?我们的整个架构都依赖于它;这是强制所有内容(包括糟糕的第三方代码)通过我们的池分配器的唯一方法。
  • 我喜欢你关于在#define 中使用逗号运算符的想法。这不完全是 OP 想要的,但它可能是一种解决方法。
  • 你的marco不会递归扩展,还是我错过了关于逗号的重要内容?
  • @Chris Lutz - 宏永远不会递归扩展。
【解决方案5】:

我发现以下库“nvwa”对于跟踪新/删除内存泄漏非常有用 - 查看文件“debug_new”作为示例,或者直接使用它。

【讨论】:

  • 哇——这是一个很棒的发现。我不确定这是使用运算符重载是好事还是坏事,但它似乎确实有效!
  • 嗯...宏替换在一些我无法更改的第 3 方代码上生成虚假输出,显式调用“::operator new”(“::operator”仍然存在,当然,而且是假的)。有什么想法吗?
【解决方案6】:

您可以做的是重载运算符 new 并在那里获取堆栈跟踪(特定于平台)并使用堆栈信息推断调用 new 的位置。

【讨论】:

【解决方案7】:

这是我使用的:

在新的.cpp中

const char* __file__ = "unknown";
size_t __line__ = 0;

void* operator new(size_t size) {
    void *ptr = malloc(size);
    record_alloc(ptr,__file__,__line__);
    __file__ = "unknown";
    __line__ = 0;
    return ptr;
}

void delete(void *ptr)
{
   unrecord_alloc(ptr);
   free(ptr);
}

为了简洁起见,我省略了 new 和 delete 的其他定义。 “record_alloc”和“unrecord_alloc”是维护包含ptr、line和file的结构链表的函数。

在新的.hpp 中

extern const char* __file__;
extern size_t __line__;
#define new (__file__=__FILE__,__line__=__LINE__) && 0 ? NULL : new

对于 g++,“new”只展开一次。关键是“&& 0”,它使它为假并导致使用真正的新的。例如,

char *str = new char[100];

被预处理器扩展为

char *str = (__file__="somefile.c",__line__=some_number) && 0 ? NULL : new char [100];

这样文件和行号就会被记录下来,然后你的自定义新函数就会被调用。

这适用于任何形式的 new -- 只要在 new.cpp 中有相应的形式

【讨论】:

  • 你不能使用逗号运算符代替 &&0?NULL: 吗?比如,#define new (file=..,line=..),new,就像@dirkgently 建议他的回答一样?
  • 我认为使用逗号运算符会导致代码中出现问题,例如:f(new char[100]); 其中的逗号会使它们成为两个参数。但是,做(__file__=__FILE__,__line__=__LINE__,0) ? 0 : new 可能会更好。避免了不必要的&amp;&amp;操作,避免包含&lt;cstddef&gt;
  • 我喜欢这个调试版本的解决方案。它不是线程安全的,但是一些线程本地存储解决了这个问题。我认为这会使发布版本的成本有点高......但无论如何,发布版本中从来没有任何泄漏
  • 非常非常聪明如果我再想一想你可以这样做:#define new PrepareNew(FILE,LINE, other ... parameters) ; new #define delete PrepareDelete( .... );删除您可以毫不费力地将其实现为线程安全的
  • 很好的解决方案,这对于大多数情况应该足够了。然而,这个宏技巧也会扩展placement new 运算符,这是不可取的。此外,在 #include'ing 任何 STL 容器标头(如 等)之前,宏需要未定义(使用 #undef)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-06-05
  • 1970-01-01
  • 1970-01-01
  • 2011-06-26
  • 2012-01-01
相关资源
最近更新 更多