我有一些 C++ 代码,它使用
Placement New
运算符在现有缓冲区中创建具有虚拟成员的类的实例。当缓冲区位于调用函数的本地堆栈上时,它会按预期工作,但当缓冲区放置在其他位置时,它会显示编译器/链接器错误。将其归结为一个最小的示例:
鉴于此类声明:
class Test {
public:
Test() {}
virtual ~Test() {}
};
这个程序运行良好:
int main (void) {
unsigned char buf[8];
Test* x = new(buf) Test();
}
该程序无法编译:
unsigned char buf[8];
int main (void) {
Test* x = new(buf) Test();
}
产生的链接器错误是:
undefined reference to `operator delete(void*, unsigned int)'
我正在为没有 C++ 标准库 (AVR Atmega328p) 的嵌入式系统进行编码,并且我已经手动实现了
Placement New
和 Placement Delete
运算符,如下所示:
inline void* operator new (size_t, void* ptr) noexcept { return ptr; }
inline void operator delete (void*, void*) noexcept { }
我的编译器是带有
avr-g++
标准的C++17
。具有所有标志的完整编译器/链接器调用是:(具有简化的路径和名称)
avr-g++ -I"/path/lib/src" -Wall -g2 -p -pg -Os -fshort-enums -ffunction-sections -fdata-sections -funsigned-char -funsigned-bitfields -fno-exceptions -std=c++17 -mmcu=atmega328p -DF_CPU=16000000UL -MMD -MP -MF"src/main.d" -MT"src/main.o" -c -o "src/main.o" "../src/main.cpp"
avr-g++ -Wl,-Map,Name.map,--cref -mrelax -Wl,--gc-sections -L"/path/lib" -mmcu=atmega328p -o "Name.elf" ./src/main.o -lib
仅当我要实例化的类具有虚拟成员时,才会出现此问题。
你能解释一下为什么它会这样以及我如何解决这个问题以便我可以使用一些静态分配的缓冲区吗?
非常感谢!
这里的核心问题有两个:
首先,
inline void operator delete (void*, void*) noexcept { }
是无关紧要的,它是关于在放置新期间处理构造函数抛出的特殊情况。由于您禁用了例外,因此这种情况不会发生。
其次,也是最重要的一点,具有虚拟析构函数的对象具有潜在的可变大小,因此如果按常规删除,则需要跟踪其大小。编译器不知道它只能用placement new 来构造,因此会生成一个vtable,其中包含用于placement new 的析构函数(依赖于外部跟踪的大小)和解除分配析构函数(以适当的大小调用operator delete)。这个释放析构函数引用了操作符delete的传统分配。
那么为什么这适用于堆栈分配的特定情况。因为gcc优化器可以证明在这种情况下该对象从未被实际使用过,并且初始化该对象的所有工作包括设置vtable都是完全没有价值的,因此将其优化掉。然后删除对删除析构函数的唯一引用,这是对运算符删除的唯一引用。
对于全局变量,gcc 优化器假设它可以在其他地方引用。即使在这种情况下事实并非如此,它也会这样做,因为 gcc 的优化器专注于大型程序,而像这样证明很简单的琐碎情况大多被忽略。
但是,如果您要对这种类型进行任何有用的工作,利用它具有虚拟析构函数的事实,则可能会导致它被初始化,并遇到相同的问题,无论它是如何分配的。