假设我们有一些虚类
Callable
,(不是抽象的,因为我们不能使用抽象类作为std::optional<T>
的类型),以及一些接受基类call_option
的函数(std::optional
)。为什么在那种情况下总是调用基类实现Callable::call()
,而不是重写Square::call()
-如call_as_pointer()
?
此行为是在将代码从 Rust 移植到 C++ 时发现的:D 我知道,最好在此处使用指针而不是
std::option
,以使其按预期工作。我修复了我的代码以避免出现意外行为,但问题是开放的 - 为什么它会这样?
#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;
}
--> 编译器资源管理器 <--
std::optional<T>
按值包含 T
类型的对象。 call_option(option)
调用用于转换两个 optional
类型的构造函数。这又调用构造函数Callable(const Square&)
,因为Square
继承自Callable
,成为隐式定义的复制构造函数Callable(const Callable&)
.
正如其他人在评论中指出的那样,这称为 object slicing。新对象是一个纯
Callable
对象,从 Callable
“部分” Square
对象初始化。在这种情况下,它有点良性,但它可能会产生更糟糕的效果,因为它会复制(或更糟糕的是,移动)对象的一部分而忽略其他对象。
首先要防止这种情况意外发生,有两个相当简单的解决方案:
struct Callable
{
virtual int call(int x) const
{
printf("default Callable::call()\n");
return x;
}
virtual ~Callable() = 0;
};
Callable::~Callable() = default;
这可以防止编译时出现类似 “无法将字段
std::_Optional_payload_base<[…]>
声明为抽象类型 Callable
” 的错误。需要明确的是,任何虚拟抽象方法都可以工作。添加虚拟析构函数有额外的好处,现在可以安全地通过指向 Square
的指针删除 Callable
对象。在你的代码中,像 std::unique_ptr<Callable> c = std::make_unique<Square>()
这样的东西会调用错误的析构函数,现在它可以工作了。
struct Callable
{
virtual int call(int x) const
{
printf("default Callable::call()\n");
return x;
}
protected:
Callable() = default;
Callable(const Callable&) = default;
~Callable() = default;
Callable& operator=(const Callable&) = default;
// add move constructors and assignment as needed
};
这同样可以防止创建这种类型的对象,并防止通过错误的析构函数破坏
Squares
(unique_ptr<Callable>
不编译)。如果基类没有虚方法并且您想避免添加虚方法的成本,则此模式最有用。
使用这些模式中的任何一个都是一个编码风格问题。因此,它是基于意见的。但是,在我看来所有被设计为继承的类都应该在这种模式中功能抽象以防止发生这个问题。
回到
void call_option(const std::optional<Callable>& measurable)
应该如何声明的问题。
在我看来你的
call_as_pointer
完全正确。如果您所做的只是“可选地借用”引用,那么任何形式的智能指针(如 unique_ptr
)都是毫无意义的、缓慢的和限制性的。借用对象是原始指针最后剩下的用途之一。
即使您将
Square
对象保存在 unique_ptrs
中,您也应该在此处将它们作为原始指针传递给这样的实例,以 1) 避免额外的间接级别,2) 明确表示感兴趣的是对象本身,而不是指向它的指针 3)避免将自己限制在 unique_ptrs
以防您希望在某些调用站点上分配堆栈或 shared_ptr
对象。
有人可能认为这是一个不错的选择:
call_option(std::optional<const Callable*> measurable)
。但是,没有什么可以阻止measurable
“具有价值”并且该价值是nullptr
。事实上,call_option(nullptr)
可能不会做你想要的。