首先让我说明我理解虚方法的工作原理(多态,后期绑定,vtable)。
我的问题是我是否应该将我的方法设为虚拟。我将举例说明我在特定情况下的困境,但任何一般指导方针也会受到欢迎。
背景信息:
我正在创建一个库。在这个库中,我有一个类CallStack
,它捕获一个调用堆栈,然后提供对捕获的堆栈帧的类似矢量的访问。捕获由protected
方法CaptureStack
完成。如果库的用户希望实现另一种捕获堆栈的方法,则可以在派生类中重新定义此方法。为了清楚起见,使方法virtual
的讨论仅适用于我知道可以在派生类中重新定义的一些方法(在本例中为CaptureStack
和destructor
),而不是所有的班级方法。
在我的库中,我使用CallStack
个对象,但从未作为指针或参考参数公开,因此只考虑使用我的库而不需要virtual
。
当有人想要使用CallStack
作为指针或引用来实现多态时,我无法想到这种情况。如果有人想要派生CallStack
并重新定义CaptureStack
,我认为只使用派生类对象就足够了。
现在只是因为我不能想到需要多态,我不应该使用virtual
方法,或者我应该使用virtual
,而不仅仅是因为可以重新定义方法。
示例如何在我的库外使用CallStack
:
if (error) {
CallStack call_stack; // the constructor calls CaptureStack
for (const auto &stack_frame : call_stack) {
cout << stack_frame << endl;
}
}
重新定义CaptureStack
的派生类可以以相同的方式使用,不需要多态:
if (error) {
// since this is not a CallStack pointer / reference, virtual would not be needed.
DerivedCallStack d_call_stack;
for (const auto &stack_frame : d_call_stack) {
cout << stack_frame << endl;
}
}
答案 0 :(得分:2)
如果你的库在构造函数中保存了调用堆栈,那么你不能使用虚方法。
这是C ++。从另一种语言转向C ++时,人们经常出错的一件事是在构造函数中使用虚方法。这从未按计划运作。
C ++在每次构造函数调用期间设置虚函数表。这意味着从构造函数调用时,函数永远不会虚拟。虚方法始终指向正在构造的当前类。
因此,即使您使用虚方法捕获堆栈,构造函数代码也总是会调用基类方法。
为了使它工作,你需要从构造函数中取出调用并使用类似的东西:
CallStack *stack = new DerivedStack;
stack.CaptureStack();
您的代码示例都没有显示使CaptureStack成为虚拟的充分理由。
答案 1 :(得分:1)
在决定是否需要virtual
函数时,您需要查看派生和覆盖函数是否会改变您现在正在实现的其他函数的预期行为/功能。
如果您依赖于同一个类的其他进程中该特定函数的实现,就像同一个类的另一个函数一样,那么您可能希望将该函数设置为虚函数。但是如果你知道你的父类应该做什么功能,并且你不希望任何人在你担心的情况下改变它,那么它就不是virtual
函数。
或者作为另一个例子,假设某人从您的实现派生一个类,覆盖一个函数,并将该对象作为一个父类传递给您自己实现的函数/类之一。您是否希望拥有该函数的原始实现,或者您希望它们使用自己的覆盖实现?如果是后者,那么你应该去virtual
,除非没有。
答案 2 :(得分:1)
我不清楚调用CallStack
的位置。从
您的示例,看起来您正在使用模板方法
模式,其中基本功能在实现
基类,但通过虚函数自定义
(通常是私人的,不受保护的)由。提供
派生类。在这种情况下(正如Peter Bloomfield指出的那样),
函数必须是虚函数,因为它们将被调用
在基类的成员函数内;因此,有一个静态的
类型CallStack
。但是:如果我理解你的例子
正确地,对CallStack
的调用将在构造函数中。
这在CallStack
的构建过程中不起作用
对象的动态类型是CallStack
,而不是
DerivedCallStack
,虚函数调用将解析为
CallStack
。
在这种情况下,对于您描述的用例,使用的解决方案
模板可能更合适。甚至......的名字
上课很清楚。我想不出任何合理的情况
不同的实例应该有不同的捕获方式
在单个程序中调用堆栈 。这表明链接时间
该类型的分辨率可能是合适的。 (我用
编译防火墙成语和我自己的链接时间分辨率
StackTrace
上课。)
答案 3 :(得分:1)
我的问题是我是否应该将我的方法设为虚拟。我将举例说明我在特定情况下的困境,但任何一般指导方针也会受到欢迎。
一些指导原则:
如果您不确定,则不应该这样做。很多人会告诉你,你的代码应该易于扩展(因此,虚拟),但在实践中,大多数可扩展代码永远不会扩展,除非你创建一个将被大量使用的库(参见YAGNI原则)。 p>
在很多情况下,你可以使用封装代替继承和类型多态(模板)作为类层次结构的替代(例如std :: string和std :: wstring不是基本字符串类的两个具体实现并且它们根本不可继承。)
如果(当你设计你的代码/公共接口时)你意识到你有多个类“是”另一个类的接口的实现,那么你应该使用虚函数。
< / LI>答案 4 :(得分:0)
您几乎肯定会将该方法声明为virtual
。
第一个原因是你的基类中调用CaptureStack
的任何东西都将通过基类指针(即本地this
指针)这样做。因此,它将调用函数的基类版本,即使派生类掩盖了它。
考虑以下示例:
class Parent
{
public:
void callFoo()
{
foo();
}
void foo()
{
std::cout << "Parent::foo()" << std::endl;
}
};
class Child : public Parent
{
public:
void foo()
{
std::cout << "Child::foo()" << std::endl;
}
};
int main()
{
Child obj;
obj.callFoo();
return 0;
}
使用该类的客户端代码只使用派生对象(不是基类指针等)。但是,它实际上是被调用的foo()
的基类版本。解决这个问题的唯一方法是使foo()
虚拟。
第二个原因只是正确的设计之一。如果派生类函数的目的是覆盖而不是掩盖原始函数,那么它应该这样做,除非有特殊原因(例如性能问题) 。如果你不这样做,你将来会引发错误和错误,因为这个类可能没有按预期行事。