【问题标题】:Returning a reference to the derived class from a base class method从基类方法返回对派生类的引用
【发布时间】:2019-06-03 08:00:29
【问题描述】:

我的任务是实现一个简单的 SVG 生成器。我需要支持圆形、折线和文本。这三者至少有 4 种常用方法: - 设置描边颜色 - 设置填充颜色 - 设置描边宽度 - ToString 主要要求之一是支持链接,例如: 折线{}.SetStrokeColor("white").SetFillColor("black")...

我决定实现一个基类 Element,所有其他类都继承自它。这个想法是有一个类 Document ,它包含添加到文档中的所有元素的向量。 基本方法的示例签名:

// source of trouble
Element &SetStrokeColor(const Color &color) {
    ...
    return *this;
}

我的派生类确实调用了这些方法,但问题是这些方法返回的是对基类 Element 的引用,而不是派生类。

我的问题是是否可以一起用 c++ 实现???

Further discussion here

【问题讨论】:

  • 请花一些时间刷新the help pages,重新获取the SO tour,并重新阅读how to ask good questions,以及this question checklist。最后请不要忘记如何创建minimal reproducible example
  • 根本不是问题——只是你的签名有多余的&符号...
  • @Aconcagua 错字
  • 对基类的引用是对派生类的抽象引用。你到底想达到什么目的?鉴于您想将所有元素存储在一个向量中,您以后只能访问Element
  • 错字-确定;请注意,它仍然生成有效的 c++,并且您会返回一个右值引用...

标签: c++ inheritance


【解决方案1】:

如果您想共享实现保留类型信息,那么您需要 CRTP:

struct ElementBase { };

template <class Concrete>
struct Element : ElementBase {

    Concrete &setStrokeWidth(int width) {
        // Actual implementation...
        (void) width;

        return cthis();
    }

private:
    friend Concrete;
    Element() = default;

    Concrete &cthis() { return static_cast<Concrete &>(*this); }
    Concrete &cthis() const { return static_cast<Concrete const &>(*this); }
};

struct Circle : Element<Circle> {
    Circle &SetCircleCenter(int x, int y) {
        // Actual implementation...
        (void) x;
        (void) y;

        return *this;
    }
};

int main() {
    Circle c;
    c.setStrokeWidth(4).SetCircleCenter(0, 0);
}

See it live on Wandbox

【讨论】:

  • 缺少一点点:如何将 Element 类型的对象放入向量>?
  • 找到答案:从非模板基类派生元素。制作 unique_ptr。您介意将它添加到您的解决方案中,这样其他人就不必通读 cmets 了吗?
  • @magom001 当然可以,但请注意,由于 lubgr 的回答中描述的问题,您将无法从基类动态调用函数。不过,如果您需要,它可以被规避。
  • 当然,在我的特殊情况下,我只需要调用一个 ToString 方法,我在基类中声明纯虚拟并在子类中覆盖。这就是链接的目的:您调用 Svg::Document::Add(Svg::Circle{}.SetCenter({0,0}).SetRadius(2)),Add 方法执行 vector.emplace_back( make_unique(circle)) 当我需要生成一个 svg 时,我只需遍历 vector 中的所有元素并在每个元素上调用 ToString。
【解决方案2】:

使用协变返回类型,您可以

class Element {
  public:
    // ...

    virtual Element& refToThis() { return *this; };
};

在派生类中

class Derived : public Element {
  public:
    // ...

    Derived& refToThis() override { return *this; };
};

当静态类型为 Derived(例如在 Derived 自身内部)时,它可以让您将 Derived 实例作为 Derived 实例处理。当静态类型为Element时,refToThis()的返回类型也是。

【讨论】:

  • 感谢您的回答!我还是不明白。基本方法不仅仅是简单地返回一个引用。它做了一些逻辑等。这是否意味着我必须为每个派生类重新实现方法?
  • 是的,如果你想让派生类型通过SomeDerivedClass::refToThis()可用,那么每个子类都必须实现这个成员函数。
【解决方案3】:

比较:

class Base {};
class Derived : public Base {};

Derived d;
Base* p = &d; // points to a Base that in reality is a derived
Base& b =  d; // in this respect, references do not differ...

// and you can get the original type back:
auto dr = static_cast<Derived&>(b);  // if you know 100% for sure that b is a Derived
auto dp = dynamic_cast<Derived*>(p); // if p might point to another type; you get
                                     // a null pointer back, if it does

返回指向this/*this 的指针或引用绝对没有区别,所以是的,您可以安全地这样做。

编辑:

Circle().SetStrokeWidth(16).SetCircleCenter({0, 0})。 SetStrokeWidth 返回对 Element 的引用,因此 SetCircleCenter 不可用。

不过,在这种情况下,您会遇到一些麻烦。就像lubgr denoted 一样,您可以通过覆盖协变返回类型来解决问题——是的,这意味着您必须分别覆盖每个函数。或者,您可以使用 CRTP 保存所有这些方法:

template <typename T>
class SomeAppropriateName : public Element
{
public:
    T& setStrokeWidth(unsigned int width)
    {
        Element::setStrokeWidth(width);
        return static_cast<T&>(*this);
    }
};

class Circle : public SomeAppropriateName<Circle>
{
    // the overrides needed are inherited...
};

对于这种方法,来自Element 类的原始函数需要是非虚拟的(实际上是一个优势,因为虚拟函数调用的成本更高......);在模板中,参数(T,即Circle)尚未完全定义(使用 CRTP 模式),编译器将无法检查返回类型是否真的是协变的。所以实际上,我们只是遮蔽了基类的函数。

与覆盖相比,这仍然没有缺点:只有在对象上直接调用(虚拟)函数时,协变返回类型才可用:

Circle c;
c.setStrokeWidth(7).setCenter();

Element& e = c;
e.setStrokeWidth(7).setCenter(); // fails, even if you implement co-variant override directly

【讨论】:

  • 这个,唉,不起作用,因为Circle(又名T)在声明setStrokeWidth时并不完整,并且它的继承图是未知的。因此无法验证返回类型的协方差,代码无法编译。
  • 仍然没有骰子(我在写答案时偶然发现了它......)——即使没有明确的override,签名也会匹配,因此会执行检查。我认为这只能通过为函数使用另一个名称(或签名)来解决,à la NVI。这是demo of the issue
  • 编辑:等一下,你的代码也适用于 Clang。我做错了什么……
  • 知道了:你的基类函数是not virtual。我还没疯,耶!
  • 只需在您的演示中将virtual 添加到Element::setStrokeWidth,您就会看到它:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-11
  • 2013-03-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-09-10
相关资源
最近更新 更多