ARM MCU开发:链接时如何剔除未使用的C++虚函数?
问题描述
我用C++搭配arm-none-eabi-gcc开发MCU软件,通过Wrapper类适配不同实现。目前遇到的问题是:调用Wrapper的某一个函数时,ClassA的所有虚函数(哪怕没被调用)都会被编译进二进制,连带依赖的大库代码也会被包含,占用不必要的空间。
我做了最小复现示例:
Wrapper.h
#include <stdint.h> #include "ClassA.h" class Wrapper { public: static float square(float x); static float reciprocal(float x); static void setImplementation(ClassA *impl); private: static ClassA *classA; };
Wrapper.cpp
#include "Wrapper.h" ClassA *Wrapper::classA = nullptr; float Wrapper::square(float x) { return classA->square(x); } float Wrapper::reciprocal(float x) { return classA->reciprocal(x); } void Wrapper::setImplementation(ClassA *impl) { classA = impl; }
ClassA.h
#include <stdint.h> class ClassA { public: virtual ~ClassA() = default; virtual float square(float x); virtual float reciprocal(float x); };
ClassA.cpp
#include "ClassA.h" float ClassA::square(float x) { float y = x * x; return y; } float ClassA::reciprocal(float x) { float y = 1.0f / x; return y; }
调用代码:
ClassA classA; Wrapper::setImplementation(&classA); static volatile float value = 0.6f; static volatile float test = Wrapper::reciprocal(value);
编译链接命令:
arm-none-eabi-g++ -mcpu=cortex-m0plus -march=armv6-m -mthumb -mlittle-endian -mfloat-abi=soft \ -ggdb -Os -flto=auto -ffunction-sections -fdata-sections -fsingle-precision-constant \ -fno-rtti -fno-exceptions -Wno-register \ -Wno-pedantic -Wno-unused-parameter -Wall -Wextra -Wpedantic -Wdouble-promotion \ -DARM_MATH_CM0 -DCBOR_NO_HALF_FLOAT_TYPE -DDEBUG \ -nostartfiles --specs=nano.specs --specs=nosys.specs -Wl,--gc-sections,-no-wchar-size-warning \ -Wl,-e,main,-Map=output.map -T/workspaces/mspm0g1519.lds \ -I/workspaces/src/sandbox \ -o output.elf \ /workspaces/src/main.cpp \ /workspaces/src/sandbox/ClassA.cpp \ /workspaces/src/sandbox/Wrapper.cpp
现象:即便只调用了reciprocal,ClassA的两个函数都会被包含进二进制,正常的未使用函数剔除功能失效。
问题原因
核心原因是虚函数表的特性:
- 当定义包含虚函数的类(比如ClassA)时,编译器会生成一张虚函数表(vtable),表中会列出所有虚函数的地址,包括未被直接调用的函数。
- 只要实例化了ClassA(代码中
ClassA classA;),或者存在指向ClassA的指针/引用并可能触发多态调用(Wrapper中使用ClassA*),链接器会认为虚函数表被引用,而虚函数表又关联了所有虚函数,因此这些函数都会被标记为“被使用”,无法被--gc-sections垃圾回收。 - 即便只调用了其中一个虚函数,虚函数表本身需要完整存在以支持多态机制,连带所有关联的虚函数都会被保留。
解决办法
1. 拆分接口类(推荐,适配动态切换场景)
把大的ClassA拆分成多个单一职责的接口类,让Wrapper只持有需要的接口指针:
// ISquare.h class ISquare { public: virtual ~ISquare() = default; virtual float square(float x) = 0; }; // IReciprocal.h class IReciprocal { public: virtual ~IReciprocal() = default; virtual float reciprocal(float x) = 0; }; // ClassA同时实现两个接口 class ClassA : public ISquare, public IReciprocal { public: float square(float x) override { return x*x; } float reciprocal(float x) override { return 1.0f/x; } }; // 修改Wrapper,按需持有对应接口 class Wrapper { public: static float reciprocal(float x) { return reciprocalImpl->reciprocal(x); } static void setReciprocalImpl(IReciprocal* impl) { reciprocalImpl = impl; } private: static IReciprocal* reciprocalImpl; };
这样当只使用reciprocal功能时,只有IReciprocal相关的虚函数和实现会被链接,square的代码会被垃圾回收。
2. 使用模板代替虚函数(适合编译期确定实现的场景)
如果不需要运行时动态切换实现,可以用模板Wrapper来避免虚函数表:
template<typename Impl> class Wrapper { public: static float square(float x) { return Impl::square(x); } static float reciprocal(float x) { return Impl::reciprocal(x); } }; // 使用时指定实现 Wrapper<ClassA>::reciprocal(value);
编译器会在编译期直接实例化用到的函数,未使用的函数会被优化掉,完全避免虚函数表的问题。
3. 手动标记未使用虚函数为弱符号
在未使用的虚函数上添加__attribute__((weak)),让链接器在没有引用时可以剔除:
// ClassA.h virtual float square(float x) __attribute__((weak));
但这种方法依赖编译器行为,且如果有子类重写该函数可能出问题,仅适合临时应急。
4. 避免实例化完整基类
如果只需要某一个功能,可以创建仅实现该功能的子类,而不是实例化完整的ClassA:
class ReciprocalOnly : public ClassA { public: float reciprocal(float x) override { return 1.0f/x; } };
内容的提问来源于stack exchange,提问作者Martin
相关产品推荐
相关产品推荐

