在基类中返回抽象类型

10
在一个类层次结构的设计中,我使用了一个声明了各种派生类将要实现方法的抽象基类。从某种意义上说,这个基类在C++中就像是接口。但是,有一个具体的问题。考虑下面声明我们接口类的代码:
class Interface {
public:
    virtual Interface method() = 0;
};

class Implementation : public Interface {
public:
    virtual Implementation method() { /* ... */ }
};

当然,这段代码无法编译,因为在C++中你不能返回一个抽象类。为了解决这个问题,我使用了以下解决方案:
template <class T>
class Interface {
public:
    virtual T method() = 0;
};

class Implementation : public Interface<Implementation> {
public:
    virtual Implementation method() { /* ... */ }
};

这个解决方案可行且完美,但在我看来,它并不是很优雅,因为接口的参数存在多余的文本。如果你们能指出这个设计中的其他技术问题,我会很高兴,但这是我目前唯一关心的问题。
有没有办法消除这个多余的模板参数?可能使用宏?
注意:涉及的方法必须返回一个实例。我知道如果 method() 返回指针或引用就不会有问题。

1
你正在使用的习语被称为奇异递归模板模式。我猜你可以用宏来替换类声明,比如#define DERIVE_TEMPLATE_BASE(Derived, Base) class Derived : public Base<Derived>,但这看起来非常丑陋,可能会让你的编辑器混淆。底线是 - 是的,存在冗余,但这是一个被广泛接受和可识别的惯用语。 - gwiazdorrr
@gwiazdorrr:这似乎并不那么糟糕。如果这是一个公认的习语,那么我可以假设它对“接口”用户来说不会太陌生,对吗? - Zeenobit
1
还要注意的是,在这个例子中,没有必要将调用函数声明为虚函数,因为CRTP需要知道最终派生类型才能使用基类类型。 - Mooing Duck
@MooingDuck:这是一个有趣的效果。析构函数呢? - Zeenobit
只要您不调用 Interface<Implementation> 的析构函数,那么就没有关系。而且您也不会这样做,因为使用 std::vector<Implementation> 而不是 std::vector<Interface<Implementation>> 没有任何理由。 - Mooing Duck
2个回答

5

Interface::method() 不能返回一个没有使用指针或引用的 Interface 实例。返回非指针、非引用的 Interface 实例需要实例化 Interface 本身,这是不合法的,因为 Interface 是抽象的。如果你想让基类返回一个对象实例,你必须使用以下其中之一:

一个指针:

class Interface
{
public:
  virtual Interface* method() = 0;
};

class Implementation : public Interface
{
public:
  virtual Interface* method() { /* ... */ }
};

一个参考:

class Interface
{
public:
  virtual Interface& method() = 0;
};

class Implementation : public Interface
{
public:
  virtual Interface& method() { /* ... */ }
};

模板参数:

template<type T>
class Interface
{
public:
  virtual T method() = 0;
};

class Implementation : public Interface<Implementation>
{
public:
  virtual Implementation method() { /* ... */ }
};

5

虽然由于明显的原因你不能通过值来返回,但是完全可以返回指针或引用 -- 这被称为“协变返回类型”,它是一种有效的虚函数重载形式:

struct Base { virtual Base * foo(); }
struct Derived : Base { virtual Derived * foo(); }

重点在于 Derived::foo() 是真正的 覆盖,而不是基类隐藏的重载,因为 Derived* 是指向 Base 的派生类的指针。对于引用也是同样的道理。
换句话说,如果有一个 Base * p,并且您调用 p->foo(),则您始终可以将结果视为指向 Base 的指针(但如果您有其他信息,例如您的类实际上是 Derived,那么您可以使用该信息)。 与之相反的组合顺序,即“逆变参数类型”,不作为 C++ 的一部分允许。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接