C++中的多态方法链。

3

是否可能编写流畅的链式方法并返回派生类型?考虑以下两个类:

class Base {
protected:
    std::string mFoo;
public:
    Base& withFoo(std::string foo) {
        mFoo = foo;
        return *this;
    }
};

class Derived : public Base {
protected:
    std::string mBar;
public:
    Derived& withBar(std::string bar) {
        mBar = bar;
        return *this;
    }

    void doOutput() {
        std::cout << "Foo is " <<
            mFoo << ".  Bar is " <<
            mBar << "." << std::endl;
    }
};

我希望能创建我的对象并像下面这样使用它:
Derived d;
d.withFoo("foo").withBar("bar").doOutput();

当然,这种方法会失败,因为withFoo返回一个Base。由于我所有的with方法都只是设置成员变量,所以我可以先指定派生的with。问题在于我的构建器方法(如上面的doOutput)将需要成为一个单独的语句。
Derived d;
d.withBar("this is a bar")
    .withFoo("this is my foo");
d.doOutput();

我的问题是,是否有一种方法让withFoo返回一个未知的派生类型,以便Base可以无缝地与多个派生类一起使用(毕竟* this是一个Derived,虽然Base(正确地)不知道这件事)。
举个更具体的例子,我正在编写一些访问REST服务器的类。我有一个带有withUrl方法的RestConnection类,一个带有withParamdoPost方法的PostableRest类,以及一个带有doGet方法的GettableRest类。 我怀疑这是不可能的,可能会尝试将一堆虚拟方法塞进RestConnection中,但当有多个重载的withParam时,其中一些在GET参数列表中包含没有意义,所以我不想这么做。
提前致谢!

1
你可以给基类一个模板参数,然后派生类在继承基类时将自己作为参数传递。现在,基类可以返回对此模板参数的引用。 - Neil Kirk
1
你可能会对装饰器模式感兴趣。 - Jarod42
你是否也需要从Base到各种派生类的运行时多态性?另请注意,通常来说,受保护的属性(而不是方法)是一种强烈的设计气味,因为它们容易违反不变量。 - Mark B
你实际上具有类型擦除(也许是一个C回调函数,一些类型函数void*对象)就可以了。 - user2249683
4个回答

5

我认为在这里可以利用好夏奇异递归模板模式(CRTP),像下面这样,派生类告诉基类它的类型:

class Base
{
    // Abstract/virtual interface here.
};

template <class Derived>
class Base_T : public Base
{
private:
    std::string mFoo;

public:
    Derived& withFoo(std::string foo) {
        mFoo = foo;
        return *static_cast<Derived*>(this);
    }
};

class Derived : public Base_T<Derived> {
private:
    std::string mBar;
public:
    Derived& withBar(std::string bar) {
        mBar = bar;
        return *this;
    }

    void doOutput() {
        std::cout << "Foo is " <<
            mFoo << ".  Bar is " <<
            mBar << "." << std::endl;
    }
};

马克,你认为在这种情况下使用static_cast比声明虚基析构函数并使用dynamic_cast<Derived&>(*this)更好吗?就像这个例子一样。 - WhozCraig
@WhozCraig 我绝对推荐使用 static_cast,因为继承保证了在所有情况下类型都是正确的。dynamic_cast会增加额外开销而没有额外的好处。 - Mark B
抱歉,我之所以问是因为每次看到转换操作的反引用时,我都会感到恐慌。另外(无关紧要的),在你的示例中,“Base”不是一个模板,我认为你的意思是“class Derived : public Base_T<Derived>”,但在这里对我来说还太早了,我不知道自己在说什么 =P(+1 用于 CRTP 方法) - WhozCraig
这正是我所需要的!我已经开始使用模板,但我已经很久没有接触过c++(主要使用Java)。唯一我自己没能想出来的部分是withFoo的返回类型,但这给了我预期的结果。静态转换有什么注意事项吗? - asmodean
@WhozCraig,你说得对,我确实错过了Base_T的继承 - 已在答案中修复。 - Mark B

0

由于您的类型不是多态的(没有虚函数),因此基类对派生类一无所知。

您可以通过静态多态性实现您的目标:

template<class Derived>
class Base {
protected:
    std::string mFoo;
public:
    Derived& withFoo(std::string foo) {
        mFoo = foo;
        return static_cast<Derived&>(*this);
    }
};

class Derived : public Base<Derived> {
protected:
 ......
}

缺点是不再有一个Base类,而是有许多可能的派生类实例化,因此您不能再拥有指向任何Derived的Base&或Base*。

如果您需要一个公共基础来收集,您需要另一个非模板化的CommonBase,从中Base可以派生出来。

另一种可能性是通过使withFoo虚拟来使Base(旧版)具有多态性。 在那时,在Derived中,您可以覆盖withFoo以返回Derived&协变类型:

class Base
{
   ...
   virtual Base& withFoo(...);
   ...
   virtual ~Base() {} //just to make all the hierarchy destructible through base
};

class Derived: public Base
{
   ...
   virtual Derived& withFoo(type arg)
   { return static_cast<Derived&>(Base::withFoo(arg)); }
   ...
};

这仍然采用经典的OOP范例,但增加了运行时开销(vtables),并且缺点是基于一个特性(虚函数的协变返回类型),并非所有编译器都支持。


0

看一下奇妙的递归模板模式

如果Base是一个抽象类型(只在其子类中实例化),那么将其作为模板类型名称。您的Derive将扩展该模板 - 例如Derived:public Base<Derived>。如果Base是一个具体类型,则需要引入一个新的抽象类,该类将成为BaseDerived的基类型。

这样withFoo可以被模板化以返回真实类型。


0

你的选择要么是使用CRTP(如Mark B所示),要么是在变量名称上使用运行时分派,例如。

Derived d;
d.with("Foo", "foo").with("Bar", "bar").doOutput();

这不会特别高效,但非常灵活,并且适用于可以使用任意字段的协议。


这其实不错。其他答案更加针对我的问题,但是我可以看到这在各种情况下都有用处,特别是在设计阶段当事物可能会变动的时候。我想给这个回答点赞,但是因为声望不够所以不能 :/ - asmodean

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