Виртуальный деструктор перемещает объект из раздела родата

У меня есть большое количество статических константных объектов, которые создаются с помощью конструктора 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, указатели на них и где они хранятся и т.д.