使用下面给出的结构定义…
struct A {
virtual void hello() = 0;
};
方法# 1:
struct B : public A {
virtual void hello() { ... }
};
方法# 2:
struct B : public A {
void hello() { ... }
};
这两种重写hello函数的方法有什么区别吗?
使用下面给出的结构定义…
struct A {
virtual void hello() = 0;
};
方法# 1:
struct B : public A {
virtual void hello() { ... }
};
方法# 2:
struct B : public A {
void hello() { ... }
};
这两种重写hello函数的方法有什么区别吗?
当前回答
当你有模板并开始将基类作为模板参数时,有一个相当大的区别:
struct None {};
template<typename... Interfaces>
struct B : public Interfaces
{
void hello() { ... }
};
struct A {
virtual void hello() = 0;
};
template<typename... Interfaces>
void t_hello(const B<Interfaces...>& b) // different code generated for each set of interfaces (a vtable-based clever compiler might reduce this to 2); both t_hello and b.hello() might be inlined properly
{
b.hello(); // indirect, non-virtual call
}
void hello(const A& a)
{
a.hello(); // Indirect virtual call, inlining is impossible in general
}
int main()
{
B<None> b; // Ok, no vtable generated, empty base class optimization works, sizeof(b) == 1 usually
B<None>* pb = &b;
B<None>& rb = b;
b.hello(); // direct call
pb->hello(); // pb-relative non-virtual call (1 redirection)
rb->hello(); // non-virtual call (1 redirection unless optimized out)
t_hello(b); // works as expected, one redirection
// hello(b); // compile-time error
B<A> ba; // Ok, vtable generated, sizeof(b) >= sizeof(void*)
B<None>* pba = &ba;
B<None>& rba = ba;
ba.hello(); // still can be a direct call, exact type of ba is deducible
pba->hello(); // pba-relative virtual call (usually 3 redirections)
rba->hello(); // rba-relative virtual call (usually 3 redirections unless optimized out to 2)
//t_hello(b); // compile-time error (unless you add support for const A& in t_hello as well)
hello(ba);
}
它的有趣之处在于,您现在可以在定义类之后定义接口函数和非接口函数。这对于库之间的交互接口非常有用(不要将其作为单个库的标准设计过程)。对你的所有类都允许这样做不需要任何代价——如果你愿意,你甚至可以将B类型定义为一些东西。
注意,如果你这样做,你可能也想将复制/移动构造函数声明为模板:允许从不同的接口构造,允许你在不同的B<>类型之间进行“强制转换”。
是否应该在t_hello()中添加对const A&的支持是有问题的。这种重写的通常原因是从基于继承的专门化转向基于模板的专门化,主要是出于性能原因。如果您继续支持旧的接口,您几乎无法检测(或阻止)旧的使用。
其他回答
添加“virtual”关键字是很好的做法,因为它可以提高可读性,但不是必须的。在基类中声明为虚的函数,并且在派生类中具有相同的签名,默认情况下被视为“虚”函数。
在派生类中不需要virtual关键字。下面是c++标准草案(N3337)的支持文档(强调我):
10.3虚函数 如果虚成员函数vf在类Base和类Derived(直接或间接从Base派生)中声明,则声明了一个与Base::vf同名、形参类型-list(8.3.5)、cv- qualifier和ref-qualifier(或没有相同)的成员函数vf,则Derived::vf也是虚函数(无论是否这样声明),并且它覆盖Base::vf。
函数的“虚拟性”是隐式传播的,但是如果没有显式使用virtual关键字,我使用的至少一个编译器会生成警告,所以如果只是为了让编译器保持安静,你可能想使用它。
从纯粹的风格角度来看,包括virtual关键字清楚地向用户“宣传”这个功能是虚拟的。这对任何进一步子类化B而不必检查A的定义的人都很重要。对于深层类层次结构,这变得尤为重要。
当你有模板并开始将基类作为模板参数时,有一个相当大的区别:
struct None {};
template<typename... Interfaces>
struct B : public Interfaces
{
void hello() { ... }
};
struct A {
virtual void hello() = 0;
};
template<typename... Interfaces>
void t_hello(const B<Interfaces...>& b) // different code generated for each set of interfaces (a vtable-based clever compiler might reduce this to 2); both t_hello and b.hello() might be inlined properly
{
b.hello(); // indirect, non-virtual call
}
void hello(const A& a)
{
a.hello(); // Indirect virtual call, inlining is impossible in general
}
int main()
{
B<None> b; // Ok, no vtable generated, empty base class optimization works, sizeof(b) == 1 usually
B<None>* pb = &b;
B<None>& rb = b;
b.hello(); // direct call
pb->hello(); // pb-relative non-virtual call (1 redirection)
rb->hello(); // non-virtual call (1 redirection unless optimized out)
t_hello(b); // works as expected, one redirection
// hello(b); // compile-time error
B<A> ba; // Ok, vtable generated, sizeof(b) >= sizeof(void*)
B<None>* pba = &ba;
B<None>& rba = ba;
ba.hello(); // still can be a direct call, exact type of ba is deducible
pba->hello(); // pba-relative virtual call (usually 3 redirections)
rba->hello(); // rba-relative virtual call (usually 3 redirections unless optimized out to 2)
//t_hello(b); // compile-time error (unless you add support for const A& in t_hello as well)
hello(ba);
}
它的有趣之处在于,您现在可以在定义类之后定义接口函数和非接口函数。这对于库之间的交互接口非常有用(不要将其作为单个库的标准设计过程)。对你的所有类都允许这样做不需要任何代价——如果你愿意,你甚至可以将B类型定义为一些东西。
注意,如果你这样做,你可能也想将复制/移动构造函数声明为模板:允许从不同的接口构造,允许你在不同的B<>类型之间进行“强制转换”。
是否应该在t_hello()中添加对const A&的支持是有问题的。这种重写的通常原因是从基于继承的专门化转向基于模板的专门化,主要是出于性能原因。如果您继续支持旧的接口,您几乎无法检测(或阻止)旧的使用。
对于编译器来说,无论在派生类中编写虚函数还是省略它都没有区别。
但是您需要查看基类来获得此信息。因此,我建议在派生类中也添加virtual关键字,如果您想向人类显示该函数是虚的。