虚拟析构函数将对象移出 rodata 部分
Virtual destructor moves object out of rodata section
我有大量使用 constexpr 构造函数构造的静态常量对象,因此它们会立即存储在最终二进制文件中,而无需任何构造函数调用。
由于我在低 RAM 系统(STM32 MCU)上工作,我想减少这些对象的内存占用,因为它们是常量,所以将它们存储在 .rodata
部分。编译器毫无问题地解决了这个问题。
但是,现在我向基 class 添加了一个虚拟析构函数以删除编译器警告,对象存储在 .data
部分。
当然,我可以使用一些 #pragma
专门删除基 class 的编译器警告并删除虚拟析构函数,但我想知道是否有更清晰的解决方案.
展示问题的极简主义代码:
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
当你声明一个虚方法时,你添加了一个非常量指针到你的class,指向那个class的虚拟table。这个指针首先会被初始化为Object的虚table,然后在整个构造函数链中不断变化为派生的classes的虚指针。然后它将在析构函数链期间再次更改并回滚,直到它指向 Object 的 virtual table。
那将意味着您的对象不能再是纯只读对象,必须移出 .rodata。
更简洁的解决方案是省略 classes 中的任何虚函数,或者完全避免继承并使用模板将所需的虚函数调用替换为编译时调用。
对于具有虚方法的 classes,编译器必须为每个 class 定义虚表,以便根据对象的类型动态分派虚方法调用。因此,此类 classes 的每个对象都有一个指向其类型的 vtable 的隐藏指针。该指针由编译器添加到 class 而不是 const
并且在整个 ctor 和 dtor 调用链中发生变化,因此您的 instance
不是 const
并且可以'不会在 .rodata
.
一个 example 演示了通过指向 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 的实现细节、指向它们的指针以及它们的存储位置等。
我有大量使用 constexpr 构造函数构造的静态常量对象,因此它们会立即存储在最终二进制文件中,而无需任何构造函数调用。
由于我在低 RAM 系统(STM32 MCU)上工作,我想减少这些对象的内存占用,因为它们是常量,所以将它们存储在 .rodata
部分。编译器毫无问题地解决了这个问题。
但是,现在我向基 class 添加了一个虚拟析构函数以删除编译器警告,对象存储在 .data
部分。
当然,我可以使用一些 #pragma
专门删除基 class 的编译器警告并删除虚拟析构函数,但我想知道是否有更清晰的解决方案.
展示问题的极简主义代码:
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
当你声明一个虚方法时,你添加了一个非常量指针到你的class,指向那个class的虚拟table。这个指针首先会被初始化为Object的虚table,然后在整个构造函数链中不断变化为派生的classes的虚指针。然后它将在析构函数链期间再次更改并回滚,直到它指向 Object 的 virtual table。 那将意味着您的对象不能再是纯只读对象,必须移出 .rodata。
更简洁的解决方案是省略 classes 中的任何虚函数,或者完全避免继承并使用模板将所需的虚函数调用替换为编译时调用。
对于具有虚方法的 classes,编译器必须为每个 class 定义虚表,以便根据对象的类型动态分派虚方法调用。因此,此类 classes 的每个对象都有一个指向其类型的 vtable 的隐藏指针。该指针由编译器添加到 class 而不是 const
并且在整个 ctor 和 dtor 调用链中发生变化,因此您的 instance
不是 const
并且可以'不会在 .rodata
.
一个 example 演示了通过指向 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 的实现细节、指向它们的指针以及它们的存储位置等。