为何在std::optional中调用虚类虚方法会执行基类实现而非派生类重写版本?
问题分析:std::optional<基类>调用虚函数为何走基类实现?
问题描述
我们定义了非抽象虚类Callable(抽象类无法作为std::optional<T>的类型),以及接收基类std::optional的call_option函数。调用该函数时,执行的总是基类Callable::call()的实现,而非派生类Square重写的call(),但call_as_pointer函数却能正常调用派生类版本。已知使用指针可解决此问题,需要了解该现象的原因。
代码示例
#include <optional> #include <cstdio> struct Callable { virtual int call(int x) const { printf("default Callable::call()\n"); return x; } }; void call_option(const std::optional<Callable>& measurable) { if(measurable.has_value()) { measurable.value().call(42); } } void call_as_pointer(const Callable* measurable) { if(measurable != nullptr) { measurable->call(42); } } struct Square : Callable { int call(int x) const override { printf("overriden Square::call()\n"); return x * x; } }; int main() { const Square square; const std::optional<Square> option(square); call_option(option); // Output: default Callable::call() <-- UNEXPECTED call_as_pointer(&square); // Output: overriden Square::call() <-- EXPECTED return 0; }
原因解析
1. 对象切片导致多态失效
当std::optional<Square>传递给const std::optional<Callable>&参数时,发生了对象切片:
- 隐式转换过程中,
std::optional<Square>里的Square对象会被拷贝为一个Callable对象——只保留Square中属于基类Callable的部分,派生类的虚函数表指针、额外成员都会被丢弃。 - 因此
call_option中调用的是一个纯粹的Callable对象的call(),自然执行基类的默认实现。
2. 指针为何能正常工作
call_as_pointer接收的是Callable*指针:
- 指针指向的是原始的
Square对象,对象的虚函数表指针仍然指向Square类的虚表。 - 调用
call()时,C++会通过动态分发机制,根据虚表找到派生类的重写版本,这是多态的正常工作逻辑。
3. std::optional的本质原因
std::optional<T>的设计是存储T类型的完整实体对象,而非引用或指针。当用派生类对象初始化基类的std::optional时,等价于执行Callable temp = square;这样的拷贝操作,基类的存储空间无法容纳派生类的额外数据,必然触发切片。
解决思路
要在std::optional中保留多态行为,不能直接存储基类对象,需改用以下方式:
- 存储智能指针:比如
std::optional<std::unique_ptr<Callable>>,通过指针间接持有派生类对象,避免切片。 - 存储引用包装器:比如
std::optional<std::reference_wrapper<Callable>>,用包装器封装对象引用,同样能保留多态特性。
内容的提问来源于stack exchange,提问作者Inobelar
相关产品推荐
相关产品推荐

