通过“使用”单独继承的方法覆盖纯虚函数

时间:2010-08-19 02:14:00

标签: c++

只是一个小小的烦恼,因为我可以通过包装派生函数而不是使用'using'关键字解决问题,但为什么不执行以下工作(编译器告诉我'get_elem'仍然是纯虚拟的'酒吧'班。)

class Elem {};

class DerivedElem : public Elem {};

class Foo {

  public:
    virtual Elem&  get_elem() = 0;

};

class Goo {

  protected:
    DerivedElem elem;

  public:
    DerivedElem& get_elem()  { return elem; }

};


class Bar : public Foo, public Goo {

  public:
    using Goo::get_elem;

};

int main(void) {

  Bar bar;

}

干杯,

汤姆

3 个答案:

答案 0 :(得分:5)

如果Goo是一个“mixin”,旨在以特定方式实现接口Foo(可能还有其他mixins与其他实现),那么Goo可以从Foo派生(而不是Bar这样做)。

如果Goo不是设计用于实现接口Foo,那么将Bar视为已经实现了纯虚函数将是一个可怕的错误,而事实上它恰好具有相同签名的功能。如果你想要隐式接口和C ++中的“duck”类型,你可以做到,但你必须使用模板。无论是对还是错,纯虚函数都是用于显式声明的接口,并且没有显式声明Goo的get_elem函数来实现Foo::get_elem。所以它没有。

我想这并不能解释为什么原则上语言无法在Bar中定义using Goo::get_elem for Foo;或某些此类声明,以避免Bar需要包含很多包含调用的样板。

您可以使用模板执行某些操作以允许Goo在某种程度上支持此操作,而不必了解Foo:

template <typename T>
class Goo : public T {

  protected:
    DerivedElem elem;

  public:
    DerivedElem& get_elem()  { return elem; }
};

class Bar : public Goo<Foo> {};

class Baz : public Goo<Fuu> {};

其中Fuu是其他具有get_elem功能的界面。显然,Bar的作者有责任确保Goo确实实施Foo的合同,同样Baz检查合同Fuu }}

顺便说一句,这种形式的协方差有点狡猾。看看Foo,有人可能希望表达式bar.get_elem() = Elem()有效,而不是,因此违反了LSP。参考文献很有趣。 ((Foo &)bar).get_elem() = Elem()有效,但一般不起作用!它只分配给Elem子对象,因此((Foo &)bar).get_elem() = DerivedElem()也是如此。多态分配基本上是一种麻烦。

答案 1 :(得分:2)

在您的示例中,Foo和Goo是单独的类。在Bar中,来自Goo的方法get_elem与Foo中的方法完全不同,即使它们的签名匹配。

通过拥有using Goo::get_elem,您只需告诉编译器将对get_elem()的非限定调用解析为Goo中的那个。

答案 2 :(得分:1)

你遇到了C ++的许多奇怪角落之一。在这种情况下,C ++不会将从不同类继承的两个虚函数视为相同的函数,即使它们具有相同的名称和类型签名。

C ++以这种方式行事有一些很好的理由。例如,尽管它们具有相同的名称和类型签名,但这两个函数实际上并不相同。这两个函数的语义含义是不同的。

以下是一个例子:

namespace vendor1 {

class Circle {
 public:
    virtual ::std::size_t size() const { return sizeof(*this); }
};

} // namespace vendor1


namespace vendor2 {

class Shape {
 public:
    virtual double size() const = 0;
};

class Circle : public Shape {
 public:
    virtual double size() const { return radius_ * radius_ * M_PI; }
};

} // namespace vendor2

然后你试试这个:

namespace my_namespace {

class Circle : public ::vendor1::Circle, public ::vendor2::Circle {
 //  Oops, there is no good definition for size
};

所以你必须诉诸于此:

namespace my_namespace {

class Vendor1Circle : public ::vendor1::Circle {
 public:
    virtual ::std::size_t data_structure_size() const { return size(); }
};

class Vendor2Circle : public ::vendor2::Circle {
 public:
    virtual double area() const { return size(); }
};

class Circle : public Vendor1Circle, public Vendor2Circle {
 // Now size is still ambiguous and should stay that way
 // And in my opinion the compiler should issue a warning if you try
 // to redefine it
};

因此,C ++有充分的理由处理具有相同类型签名的虚函数(返回类型不是类型签名的一部分),并从两个不同的基础命名为不同的函数。

using而言......所有using指令都是“将此其他命名空间中的名称添加到此命名空间,就像在此处声明一样。”。就虚拟功能而言,这是一个空概念。它只是表明使用名称时的任何歧义都应该以不同的方式解决。它只声明一个名称,它没有定义名称。为了覆盖虚函数,需要新的定义。

OTOH,如果你像这样插入一个简单的thunk重新定义:

class Bar : public Foo, public Goo {

  public:
    virtual DerivedElem& get_elem()  { return Goo::get_elem(); }
};

一个好的编译器应该看到并且知道甚至懒得创建函数,而只是简单地摆弄虚拟表条目来做正确的事情。它可能需要为它实际发出代码并且在其地址被采用的情况下具有可用的符号,但是当通过Foo *调用时,它仍然应该能够简单地将虚拟表调整为使该函数完全消失。 / p>