Виртуальный деструктор перемещает объект из раздела родата
У меня есть большое количество статических константных объектов, которые создаются с помощью конструктора constexpr, поэтому они немедленно сохраняются в конечном двоичном файле без какого-либо вызова конструктора.
Поскольку я работаю в системе с низким объемом оперативной памяти (STM32 MCU), я хочу уменьшить объем памяти этих объектов и, поскольку они являются постоянными, вместо этого сохраните их в разделе .rodata
. Компилятор справился с этим без проблем.
Но теперь, когда я добавил виртуальный деструктор в базовый класс для удаления предупреждений компилятора, объекты хранятся в разделе .data
.
Конечно, я мог бы использовать #pragma
для конкретного удаления предупреждений компилятора для базового класса и удаления виртуального деструктора, но я хочу знать, есть ли более чистое решение для этого.
Код минимализма, демонстрирующий проблему:
class Object {
int value;
public:
constexpr Object(int param)
: value(param) {}
virtual int getValue() const = 0;
virtual ~Object() = default; // This line causes problems
};
class Derived : public Object {
volatile int otherValue;
public:
constexpr Derived(int param1, int param2)
: Object(param1), otherValue(param2) {}
int getValue() const override { return otherValue; }
};
const Derived instance(1,2);
int main() {
return instance.getValue();
}
Кроме того, вот CompilerExplorer для сравнения с виртуальным деструктором и без него: https://godbolt.org/z/M5G7LO
Ответы
Ответ 1
В тот момент, когда вы объявляете виртуальный метод, вы добавляете непостоянный указатель на ваш класс, который указывает на виртуальную таблицу этого класса. Этот указатель сначала будет инициализирован для виртуальной таблицы Object, а затем продолжит изменяться на виртуальные указатели производных классов по всей цепочке конструктора. Затем он снова изменится во время цепочки деструкторов и отката, пока он не укажет на виртуальную таблицу объектов.
Это будет означать, что ваш объект больше не может быть чисто доступным только для чтения объектом и должен выходить за пределы .rodata.
Более чистым решением было бы либо исключить любую виртуальную функцию в ваших классах, либо полностью избежать наследования и использовать шаблоны для замены требуемых вызовов виртуальных функций вызовами времени компиляции.
Ответ 2
Для классов, имеющих виртуальные методы, компилятор должен определить vtables для каждого класса, чтобы динамически отправлять вызовы виртуальных методов в зависимости от типа объекта. Таким образом, у каждого объекта таких классов есть скрытый указатель на vtable их типов. Этот указатель добавляется в класс компилятором и не является const
и изменяется по всей цепочке вызовов ctor и dtor, поэтому ваш instance
не является const
и не может быть в .rodata
.
пример, демонстрирующий доступ к виртуальным методам через указатель на vtable.
#include <iostream>
class FooBar {
public:
virtual void foo() { std::cout << "foo" << std::endl; };
virtual void bar() { std::cout << "bar" << std::endl; };
};
int main()
{
FooBar obj;
// first bytes of 'obj' is a pointer to vtable
uintptr_t vtable_ptr = ((uintptr_t*)&obj)[0];
// 'foo' is at index '0' and 'bar' is at index '1'
uintptr_t method_ptr = ((uintptr_t*)vtable_ptr)[1];
// cast it to member pointer
void (*func)(FooBar*) = (void (*)(FooBar*))method_ptr;
// invoke the member function on 'obj'
(*func)(&obj);
return 0;
}
Этот код работает только с определенными компиляторами. Также обратите внимание, что стандарт не определяет детали реализации vtables, указатели на них и где они хранятся и т.д.