I have some C++-Code which uses the Placement New
operator to create an instance of a class with virtual members in an existing buffer. It works as expected when the buffer is on the local stack of the calling function, but it shows a Compiler/Linker error when the buffer is placed anywhere else. Boiling it down to a minimal example:
Given this class declaration:
class Test {
public:
Test() {}
virtual ~Test() {}
};
This program works fine:
int main (void) {
unsigned char buf[8];
Test* x = new(buf) Test();
}
This program does not compile:
unsigned char buf[8];
int main (void) {
Test* x = new(buf) Test();
}
The resulting linker error is:
undefined reference to `operator delete(void*, unsigned int)'
I'm coding for an embedded system without a C++ standard library (AVR Atmega328p), and I have implemented the Placement New
and Placement Delete
operators manually like this:
inline void* operator new (size_t, void* ptr) noexcept { return ptr; }
inline void operator delete (void*, void*) noexcept { }
My compiler is the avr-g++
with C++17
standard. The full Compiler/Linker calls with all flags are: (with simplified paths and names)
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
The problem only occurs when the class I want to instantiate has virtual members.
Can you explain why it behaves like this and how I can solve this issue so I can use some statically allocated buffers?
Thanks a lot!
TL;DR: operator delete(void*, unsigned int)
needs to be defined, but doesn't need to actually have a meaningful implementation and should probably trap.
The core issue here is twofold:
Firstly, inline void operator delete (void*, void*) noexcept { }
is irrelevant, being about the special case of handling a constructor throwing during a placement new. Since you have exceptions disabled, this can't happen.
Secondly and most significantly, an object with a virtual destructor has potentially variable size, and so needs to track its size if conventionally deleted. The compiler does not know it will only be constructed with placement new, and so generates a vtable with both a destructor for placement new which relys on size being tracked externally, and a deallocating destructor, which calls operator delete with the appropriate size. This deallocating destructor references the conventional allocation of operator delete.
So why does this work for the specific case of stack allocation. Because the gcc optimizer can prove that the object is never actually used in that case, and that all of the work of initializing the object including setting up the vtable is completely worthless, and so optimizes it out. That then removes the only reference to the deleting destructor, which is the only reference to operator delete.
In the case of the global variable, the gcc optimizer assumes it may be referenced elsewhere. It does this even though it is provably not the case in this scenario because gcc's optimizer is focused on large programs, and trivial cases like this where the proofs are simple are mostly ignored.
However, if you were to do any useful work with this type that made use of the fact it has a virtual destructor, that would likely result in it getting initialized, and encountering the same problem regardless of how it was allocated.
As such operator delete(void*, unsigned int)
needs to be defined for the compiler to populate the vtable, but you should probably trap if it gets called if you are only interested in placement new.