15

考虑:

// member data omitted for brevity

// assume that "setAngle" needs to be implemented separately
// in Label and Image, and that Button does need to inherit
// Label, rather than, say, contain one (etc)

struct Widget {
    Widget& move(Point newPos) { pos = newPos; return *this; }
};

struct Label : Widget {
    Label& setText(string const& newText) { text = newText; return *this; }
    Label& setAngle(double newAngle) { angle = newAngle; return *this; }
};

struct Button : Label {
    Button& setAngle(double newAngle) {
        backgroundImage.setAngle(newAngle);
        Label::setAngle(newAngle);
        return *this;
    }
};

int main() {
    Button btn;

    // oops: Widget::setText doesn't exist
    btn.move(Point(0,0)).setText("Hey");

    // oops: calling Label::setAngle rather than Button::setAngle
    btn.setText("Boo").setAngle(.5); 
}

有什么技巧可以解决这些问题?

示例:使用模板魔术使 Button::move 返回 Button& 或其他东西。

编辑很明显,第二个问题是通过将 setAngle 设为虚拟来解决的。

但是第一个问题仍未以合理的方式解决!

编辑:嗯,我想在 C++ 中做正确是不可能的。无论如何感谢您的努力。

4

15 回答 15

16

您可以扩展CRTP来处理这个问题。monjardin 的解决方案朝着正确的方向发展。您现在需要的只是一个默认Label实现,以将其用作叶类。

#include <iostream>

template <typename Q, typename T>
struct Default {
    typedef Q type;
};

template <typename T>
struct Default<void, T> {
    typedef T type;
};

template <typename T>
void show(char const* action) {
    std::cout << typeid(T).name() << ": " << action << std::endl;
}

template <typename T>
struct Widget {
    typedef typename Default<T, Widget<void> >::type type;
    type& move() {
        show<type>("move");
        return static_cast<type&>(*this);
    }
};

template <typename T = void>
struct Label : Widget<Label<T> > {
    typedef typename Default<T, Widget<Label<T> > >::type type;
    type& set_text() {
        show<type>("set_text");
        return static_cast<type&>(*this);
    }
};

template <typename T = void>
struct Button : Label<Button<T> > {
    typedef typename Default<T, Label<Button<T> > >::type type;
    type& push() {
        show<type>("push");
        return static_cast<type&>(*this);
    }
};

int main() {
    Label<> lbl;
    Button<> btt;

    lbl.move().set_text();
    btt.move().set_text().push();
}

也就是说,请考虑这样的努力是否值得增加少量的语法奖励。考虑替代解决方案。

于 2009-02-21T10:56:59.033 回答
8

对于第二个问题,将 setAngle 设为虚拟应该可以解决问题。

对于第一个,没有简单的解决方案。Widget::move 返回一个 Widget,它没有 setText 方法。您可以制作一个纯虚拟 setText 方法,但这将是一个非常丑陋的解决方案。您可以在按钮类上重载 move() ,但维护起来会很痛苦。最后,您可能可以使用模板做一些事情。也许是这样的:

// Define a move helper function
template <typename T>
T& move(T& obj, Point& p){ return obj.move(p); };

// And the problematic line in your code would then look like this:
move(btn, Point(0,0)).setText("Hey");

我会让你决定哪种解决方案最干净。但是有什么特别的原因需要能够链接这些方法吗?

于 2009-02-15T17:51:47.260 回答
4

解决问题的一种简单但烦人的方法是在子类中重新实现所有公共方法。这并不能解决多态性问题(例如,如果您从 Label 转换为 Widget),这可能是也可能不是主要问题。

struct Widget {
    Widget& move(Point newPos) { pos = newPos; return *this; }
};

struct Label : Widget {
    Label& setText(string const& newText) { text = newText; return *this; }
    Label& setAngle(double newAngle) { angle = newAngle; return *this; }
    Label& move(Point newPos) { Widget::move(newPos); return *this; }
};

struct Button : Label {
    Button& setText(string const& newText) { Label::setText(newText); return *this; }
    Button& setAngle(double newAngle) {
        backgroundImage.setAngle(newAngle);
        Label::setAngle(newAngle);
        return *this;
    }
    Button& move(Point newPos) { Label::move(newPos); return *this; }
};

请务必在您的文档中包含这必须完成的链接才能正常工作。

但实际上,现在,为什么还要麻烦方法链呢?很多时候,这些函数的调用将不那么简单,并且需要更长的行。这真的会损害可读性。每行一个动作——这也是一般++规则--

于 2009-02-15T18:06:39.017 回答
4

Button真的是一个吗Label?您似乎违反了Liskov 替换原则。或许您应该考虑使用装饰器模式向小部件添加行为。

如果您坚持原样的结构,您可以像这样解决您的问题:

struct Widget {
    Widget& move(Point newPos) { pos = newPos; return *this; }
    virtual ~Widget();  // defined out-of-line to guarantee vtable
};

struct Label : Widget {
    Label& setText(string const& newText) { text = newText; return *this; }
    virtual Label& setAngle(double newAngle) { angle = newAngle; return *this; }
};

struct Button : Label {
    virtual Label& setAngle(double newAngle) {
        backgroundImage.setAngle(newAngle);
        Label::setAngle(newAngle);
        return *this;
    }
};

int main() {
    Button btn;

    // Make calls in order from most-specific to least-specific classes
    btn.setText("Hey").move(Point(0,0));

    // If you want polymorphic behavior, use virtual functions.
    // Anything that is allowed to be overridden in subclasses should
    // be virtual.
    btn.setText("Boo").setAngle(.5); 
}
于 2009-02-15T20:19:25.720 回答
4

我会放弃链接的东西。一方面,如果不做一些相对讨厌的黑客攻击,它实际上是无法完成的。但是,最大的问题是它使阅读和维护代码变得更加困难,而且你很可能最终会被人们滥用,创建一个巨大的代码行来做很多事情(回想一下高中代数和那些巨大的代码行)加法,减法和乘法,你似乎总是在某个时候结束,如果你让他们这样做,人们就会这样做)。

另一个问题是,由于系统中的大多数函数都将返回对自身的引用,因此所有这些函数都应该是合乎逻辑的。当(不是如果)你最终开始实现也应该返回值的函数时(不仅是访问器,还有一些修改器以及其他泛型函数),你将面临两难境地,要么打破你的约定(这将滚雪球,不清楚应该如何为其他未来的函数实现事情)或被迫开始通过参数返回值(我相信你会讨厌,就像我认识的大多数其他程序员一样)。

于 2009-02-25T14:52:59.387 回答
3

C++ 确实支持虚拟方法的返回值协方差。所以你可以通过一些工作得到你想要的东西:

#include <string>
using std::string;

// member data omitted for brevity

// assume that "setAngle" needs to be implemented separately
// in Label and Image, and that Button does need to inherit
// Label, rather than, say, contain one (etc)


struct Point
{
    Point() : x(0), y(0) {};
    Point( int x1, int y1) : x( x1), y( y1) {};

    int x;
    int y;
};

struct Widget {
    virtual Widget& move(Point newPos) { pos = newPos; return *this; }
    virtual ~Widget() {};

    Point pos;
};

struct Label : Widget {
    virtual ~Label() {};
    virtual Label& move( Point newPos) { Widget::move( newPos); return *this; }

    // made settext() virtual, as it seems like something 
    // you might want to be able to override
    // even though you aren't just yet
    virtual Label& setText(string const& newText) { text = newText; return *this; }
    virtual Label& setAngle(double newAngle) { angle = newAngle; return *this; }

    string text;
    double angle;
};

struct Button : Label {
    virtual ~Button() {};
    virtual Button& move( Point newPos) { Label::move( newPos); return *this; }
    virtual Button& setAngle(double newAngle) {
        //backgroundImage.setAngle(newAngle);
        Label::setAngle(newAngle);
        return *this;
    }
};

int main()
{
    Button btn;

    // this works now
    btn.move(Point(0,0)).setText("Hey");

    // this works now, too
    btn.setText("Boo").setAngle(.5); 

  return 0;
}

请注意,您应该使用虚拟方法来执行此类操作。如果它们不是虚拟方法,那么“重新实现”的方法将导致名称隐藏,并且调用的方法将取决于变量、指针或引用的静态类型,因此如果基指针或引用可能不是正确的方法正在使用参考。

于 2009-02-16T01:12:07.420 回答
2

[咆哮]

是的。退出此方法链接业务,只需连续调用函数。

说真的,你为允许这种语法付出了代价,而我没有得到它提供的好处。

[/咆哮]

于 2009-02-15T17:46:15.777 回答
2

不适用于 C++。

C++ 不支持返回类型的变化,因此即使您覆盖它,也无法将 Widget.move() 返回的引用的静态类型更改为比 Widget& 更具体。

C++ 需要能够在编译时检查事物,所以你不能使用真正从 move 返回的是一个按钮的事实。

充其量,您可以进行一些运行时强制转换,但它看起来并不漂亮。只是单独调用。

编辑:是的,我很清楚 C++ 标准说返回值协方差是合法的这一事实。然而,在我教授和练习 C++ 的时候,一些主流编译器(例如 VC++)没有。因此,为了便携性,我们建议不要这样做。最后,当前的编译器可能对此没有任何问题。

于 2009-02-15T17:46:45.057 回答
2

有一段时间,我认为可能会重载稍微不寻常operator->()的链接方法而不是.,但这步履蹒跚,因为编译器似乎要求 右侧的标识符->属于左侧表达式的静态类型. 很公平。

穷人的方法链

退一步说,方法链接的重点是避免重复输入长对象名称。我会建议以下快速而肮脏的方法:

而不是“速记形式”:

btn.move(Point(0,0)); btn.setText("Hey");

你可以写:

{Button& _=btn; _.move(Point(0,0)); _.setText("Hey");}

不,它不像真正的链接那样简洁.,但是当需要设置许多参数时它会节省一些输入,并且它确实具有不需要对现有类进行代码更改的好处。因为您将整个方法调用组包装起来{}以限制引用的范围,所以您始终可以使用相同的短标识符(例如_or x)来代表特定的对象名称,从而潜在地增加可读性。最后,编译器将毫无问题地优化 away _

于 2009-02-16T15:32:51.530 回答
1

我认为(我没有测试过)这将使用模板来完成:

template<class T> struct TWidget {
    T& move(Point newPos) { pos = newPos; return (T&)*this; }
};

template<class T> struct TLabel : TWidget<T> { ... }

struct Label : TLabel<Label> { ... }

struct Button : TLabel<Button> { ... }

笔记:

  • 任何/每个基类都需要是一个模板,顶部有一个单独的非模板叶类(对比LabelTLabel类)。
  • dynamic_cast如果你愿意,演员可以是一个。
  • return *this基类可以包含 aT&作为数据成员而不是强制转换“ ”(派生类将传递this给基类的构造函数),这将是一个额外的数据成员,但它避免了强制转换,我认为可能允许组合代替或与继承一样。
于 2009-02-15T17:55:08.937 回答
1

这在 gcc 4.3.2 上编译,是一种混合模式

#include <string>

using namespace std;

struct Point {
    Point() : x(0), y(0) {}
    Point(int x, int y) : x(x), y(y) {}

    int x, y;
};

template <typename T>
struct Widget {
    T& move(Point newPos) {
        pos = newPos;
        return *reinterpret_cast<T *> (this);
    }

    Point pos;
};

template <typename T>
struct Label : Widget<Label<T> > {
    T& setText(string const& newText) {
        text = newText;
        return *reinterpret_cast<T *> (this);
    }
    T& setAngle(double newAngle) {
        angle = newAngle;
        return *reinterpret_cast<T *> (this);
    }

    string text;
    double angle;
};

struct Button : Label<Button> {
    Button& setAngle(double newAngle) {
        backgroundImage.setAngle(newAngle);
        Label<Button>::setAngle(newAngle);
        return *this;
    }

    Label<Button> backgroundImage;
};

int main() {
    Button btn;

    // oops: Widget::setText doesn't exist
    btn.move(Point(0,0)).setText("Hey");

    // oops: calling Label::setAngle rather than Button::setAngle
    btn.setText("Boo").setAngle(0.0); 
}
于 2009-02-15T17:58:47.087 回答
1

其他人遇到了设计问题。您可以使用 C++ 对协变返回类型的支持来解决该问题(尽管以一种非常粗暴的方式)。

struct Widget {
    virtual Widget& move(Point newPos) { pos = newPos; return *this; }
};

struct Label : Widget {
    Label& move(Point newPos) { pos = newPos; return *this; }
    Label& setText(string const& newText) { text = newText; return *this; }
    Label& setAngle(double newAngle) { angle = newAngle; return *this; }
};

struct Button : Label {
    Button& setAngle(double newAngle) {
        backgroundImage.setAngle(newAngle);
        Label::setAngle(newAngle);
        return *this;
    }
};

int main() {
    Button btn;

    // oops: Widget::setText doesn't exist
    btn.move(Point(0,0)).setText("Hey");

    // oops: calling Label::setAngle rather than Button::setAngle
    btn.setText("Boo").setAngle(.5); 
}

确实,尽管以您的方式链接方法会产生奇怪的代码,并且会损害可读性而不是帮助。如果您终止了对 self junk 的返回引用,您的代码将变为:

struct Widget {
    void move(Point newPos) { pos = newPos; }
};

struct Label : Widget {
    void setText(string const& newText) { text = newText; }
    void setAngle(double newAngle) { angle = newAngle; }
};

struct Button : Label {
    void setAngle(double newAngle) {
        backgroundImage.setAngle(newAngle);
        Label::setAngle(newAngle);
    }
};

int main() {
    Button btn;

    // oops: Widget::setText doesn't exist
    btn.move(Point(0,0));
    btn.setText("Hey");

    // oops: calling Label::setAngle rather than Button::setAngle
    btn.setText("Boo");
    btn.setAngle(.5); 
}
于 2009-02-24T14:34:38.690 回答
1

实际上,由于异常安全性差,方法链接是个坏主意。你真的需要吗?

于 2009-05-10T06:13:42.187 回答
0

好吧,您知道它是 a Button,因此您应该能够将返回的转换Widget&为 aButton&并继续前进。不过看起来确实有点丑。

另一个相当烦人的选择是在你的Button类中为Widget::move函数(和朋友)创建一个包装器。如果您有多个功能,可能不值得努力包装所有内容。

于 2009-02-15T17:45:23.220 回答
0

setText() 和 setAngle() 真的需要在每个类中返回它们自己的类型吗?如果将它们都设置为返回 Widget&,那么您可以只使用虚函数,如下所示:

struct Widget {
    Widget& move(Point newPos) { pos = newPos; return *this; }
    virtual Widget& setText(string const& newText) = 0;
    virtual Widget& setAngle(double newAngle) = 0;
};

struct Label : Widget {
    virtual Widget& setText(string const& newText) { text = newText; return *this; }
    virtual Widget& setAngle(double newAngle) { angle = newAngle; return *this; }
};

struct Button : Label {
    virtual Widget& setAngle(double newAngle) {
        backgroundImage.setAngle(newAngle);
        Label::setAngle(newAngle);
        return *this;
    }
};

请注意,即使返回类型是 Widget&,按钮或标签级别的函数仍然会被调用。

于 2009-02-15T21:14:58.020 回答