我长期使用以下C ++经验法则:
如果某个类覆盖了其中的函数 基类,功能应该是 在基地宣布
virtual
。
我想我从这个规则中遇到了一个例外。为了判断这是否合理,或者指出我的设计存在缺陷,我问的是这个问题。我想得到例子或更好的规则。
编辑:我试着在这里描述我的用例,我明白我不需要继承!
我想问一个普遍的问题。谢谢你的答案!
答案 0 :(得分:12)
您无法覆盖非虚拟功能。您唯一能做的就是隐藏基类实现。但是这并没有为您提供虚函数提供的多态行为。
答案 1 :(得分:11)
我个人不喜欢这样,但有时它很有用。标准库也使用它:
stringstream ss;
/* Imagine you want to redirect all output that goes into "ss"
* to "cout". The following does NOT work! */
ss.rdbuf(cout.rdbuf());
为什么不起作用?因为stringstream
已隐藏ios::rdbuf
具有相同的命名函数,该函数仅提供对其内部std::stringbuf
的读取访问权限,而不是附加缓冲区。您需要执行以下操作
ss.std::ios::rdbuf(cout.rdbuf());
现在,附加到流的缓冲区不等于ss.rdbuf()
返回的内容。我个人不喜欢这个设计。
我曾经很好地利用了藏身之处。在我看来,隐藏需要一个要求:
在我的情况下,我有一个像这样的基类(不是真的很接近,但它传达了这种情况)。
template<typename T>
struct A {
void doSomething() {
T t;
t.doIt();
}
};
class Foo;
struct B : A<Foo> {
};
B b;
当一个人拨打b.doSomething()
时会发生什么?它需要Foo
的标题,因为该函数想要在类上调用doIt
并创建该类型的变量。解决方法很简单
class Foo;
struct B : A<Foo> {
void doSomething();
};
// and in the .cpp file:
#include "Foo.h"
void B::doSomething() {
A<Foo>::doSomething();
}
这样,我阻止了B
类的每个用户都需要包含Foo的头。只有B的cpp文件,它知道它取决于“Foo”,必须这样做。
答案 2 :(得分:2)
有可能吗?是。 这是道德的吗?这取决于你对道德的定义。 是否让您的开发人员和未来的维护程序员感到困惑?肯定是的!
在派生类中具有与基类中的非虚函数同名的函数,只需隐藏基类函数及其重载。
我认为这是滥用继承,因为你基本上是说你想要以基类合同不允许的方式重新定义某些事情的方式。通过在基类中创建非虚函数,您可以指定函数要执行的操作(其界面),更重要的是如何您希望它执行它(其实现)。使函数非虚拟的含义是它的接口及其实现都不应该在派生类中更改。
答案 3 :(得分:2)
我认为你误解了这条规则。规则是:“如果要从基类覆盖虚方法,则应将重写方法声明为虚拟方法。”
这是一种代码样式规则,可以防止混淆,因为虚拟修饰符是继承的。
答案 4 :(得分:2)
你可以做到。但可能不是你想要的。 OOP的主要原则之一是运行时多态性。在这种情况下,您可能无法使用它。
检查以下代码。它试图使用基类型对象来处理超类型对象。但非虚拟化不起作用。
我的预期输出是
In B::printNonV()
In B::printV()
但我得到了
In A::printNonV()
In B::printV()
#include <iostream>
using namespace std;
class A
{
public:
void printNonV(){
cout<<"In A::printNonV() "<<endl;
}
virtual void printV(){
cout<<"In A::printV()"<<endl;
}
};
class B:public A
{
public:
void printNonV(){
cout<<"In B::printNonV()"<<endl;
}
virtual void printV(){
cout<<"In B::printV()"<<endl;
}
};
int main(){
A* b=new B();
b->printNonV();
b->printV();
}
答案 5 :(得分:2)
当您使用CRTP实现模拟动态绑定时,从基类重载(不覆盖)非虚函数的一个示例是:
// in the shared header
template <typename Derived>
struct GenericOSDetails {
size_t preferred_character_size() {
return 1; // we expect `char` to be the preferred character type
}
size_t preferred_string_length(size_t numchars) {
return numchars * static_cast<Derived&>(*this).preferred_character_size();
}
// other functions that do considerably more useful things based on
// the preferred character size and encoding.
};
// in the linux header
struct LinuxOSDetails : GenericOSDetails<LinuxOSDetails> {
// we're happy with the defaults.
};
// in the windows header
struct WindowsOSDetails : GenericOSDetails<WindowsOSDetails> {
// configure ourselves for "Unicode" vs non-Unicode builds.
size_t preferred_character_size() {
return sizeof(TCHAR);
}
};
注意模拟动态绑定 - 使用这种技术,WindowsOSDetails的实例不会作为指向基类GenericOSDetails<WindowsOSDetails>
的指针传递,因此不需要虚函数。整个过程都使用静态绑定,但基类仍然可以调用派生类函数并获得重载版本。
说实话,我不确定这是多么实用。可能99%的时间你可能会想到使用它,或者是过早的优化,否则你应该提供一个策略作为模板参数,以及常见情况的默认策略,而不是使用继承。但是另外1%的时间,并且通常在你想要使用继承但你不想要或不需要动态多态的任何时候,如果你愿意,你可以避免使用虚函数。
AFAIK它只是在模板密集的代码中,你做了一些特别有趣的继承,而不依赖于动态多态。普通的OOP范例不感兴趣。
答案 6 :(得分:1)
我认为人们普遍认为隐藏基类功能并不是一件好事,如果有的话应该很少。更令人惊讶的问题之一是你真正打破了多态行为(如Asha所述)。这是一个为什么会出人意料的例子。
struct Person {
virtual std::string get_name() const = 0;
void print_name(std::ostream& s) const { s << get_name() << std::endl; }
};
struct Fred: Person {
virtual std::string get_name() const { return "Fred"; }
};
struct Barney: Person {
virtual std::string get_name() const { return "Barney"; }
void print_name(std::ostream& s) const { s << "Bam Bam" << std::endl; }
};
std::ostream& operator<<(std::ostream& s, Person const& p) {
p.print_name(s);
return s;
}
int main() {
Fred fred;
Barney barney;
barney.print_name(std::cout);
std::cout << fred << barney << std::endl;
return 0;
}
输出:
Bam Bam
Fred
Barney
隐藏基类会破坏Liskov Substitution Principle,这会以各种各样且通常令人不快的方式使实现感到惊讶。