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 を仮想化することで、2 番目の問題が解決されることが明らかになりました。

しかし、最初の問題は合理的な方法で未解決のままです!

編集:まあ、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

2 番目の問題については、setAngle を virtual にするとうまくいくはずです。

最初のものについては、簡単な解決策はありません。Widget::move は setText メソッドを持たない Widget を返します。純粋な仮想 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

問題を解決する簡単ですが面倒な方法は、サブクラスのすべてのパブリックメソッドを再実装することです。これは、ポリモーフィズムの問題(たとえば、ラベルからウィジェットにキャストする場合)を解決しません。これは、主要な問題である場合とそうでない場合があります。

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; }
};

チェーンを機能させるには、これをドキュメントに含める必要があります。

しかし、実際には、なぜ今、メソッドの連鎖に悩むのでしょうか。多くの場合、これらの関数はそれほど簡単には呼び出されず、より長い行が必要になります。これは本当に読みやすさを損なうでしょう。1行に1つのアクション-これは、一般的なルール++です--

于 2009-02-15T18:06:39.017 に答える
4

Button本当にLabel?_ リスコフの置換原則に違反しているようです。おそらく、ウィジェットに動作を追加するためにデコレータパターンを検討する必要があります。

構造をそのまま主張すると、次のように問題を解決できます。

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

私はチェーンのものを放棄します。1 つには、比較的厄介なハックを行わずに実際に実行することはできません。しかし、最大の問題は、コードの読み取りと保守が難しくなることです。おそらく、コードを悪用して、1 つの巨大なコード行を作成して複数のことを行うことになります (高校の代数とその巨大な行を思い出してください)。足し算、引き算、掛け算のいずれかの時点で常に発生するように思われる、それを許可した場合に人々が行うこと)。

もう 1 つの問題は、システム内のほとんどの関数がそれ自体への参照を返すことになるため、すべての関数が論理的になるということです。( if ではなく) 最終的に値を返す関数 (アクセサーだけでなく、一部のミューテーターやその他のジェネリック関数) の実装を開始すると、ジレンマに直面することになります。雪だるま式になり、他の将来の機能のためにどのように実装する必要があるかが不明確になります) またはパラメーターを介して値を返すことを強制される (私が知っている他のほとんどのプログラマーもそうであるように、あなたはこれを嫌うと確信しています)。

于 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

しばらくの間、 のoperator->()代わりにメソッドをチェーンするための少し変わった をオーバーロードすることは可能かもしれないと考えていまし.たが、コンパイラは の右側の識別子が左側の式の静的->型に属することを要求しているように見えるため、それはうまくいきませんでした。 . けっこうだ。

貧乏人のメソッド連鎖

少し前に戻りますが、メソッド チェーンのポイントは、長いオブジェクト名を繰り返し入力しないようにすることです。次の迅速で汚いアプローチをお勧めします。

「手書き形式」の代わりに:

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

あなたは書ける:

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

いいえ、これは を使用した実際の連鎖ほど簡潔ではありません.が、設定するパラメーターが多数ある場合に入力の手間を省くことができ、既存のクラスのコードを変更する必要がないという利点があります。メソッド呼び出しのグループ全体をラップして参照の範囲を制限するため、特定のオブジェクト名を表すために同じ短い識別子 (または など) を{}常に使用でき、可読性が向上する可能性があります。最後に、コンパイラは問題なく最適化して._x_

于 2009-02-16T15:32:51.530 に答える
2

[暴言]

はい。このメソッド チェーン ビジネスを終了し、関数を続けて呼び出すだけです。

真剣に、あなたはこの構文を許可するために代償を払っています。

[/暴言]

于 2009-02-15T17:46:15.777 に答える
2

C++ ではありません。

C++ は戻り値の型の分散をサポートしていないため、Widget.move() から返される参照の静的型を変更して、オーバーライドしたとしても、Widget& よりも具体的にする方法はありません。

C++ はコンパイル時にチェックできる必要があるため、move から実際に返されるのはボタンであるという事実を利用することはできません。

せいぜい、いくつかのランタイム キャストを行うことはできますが、見栄えはよくありません。通話を分けるだけです。

編集:はい、C++標準が戻り値の共分散が正当であると言っているという事実をよく知っています。しかし、私が C++ を教えたり練習したりしていたとき、一部の主流のコンパイラ (VC++ など) はそうではありませんでした。したがって、移植性のために、それをお勧めしません。最終的に、現在のコンパイラでは問題がない可能性があります。

于 2009-02-15T17:46:45.057 に答える
1

実際、例外の安全性が低いため、メソッドの連鎖はお勧めできません。本当に必要ですか?

于 2009-05-10T06:13:42.187 に答える
1

これは gcc 4.3.2 でコンパイルされ、一種のmixin パターンです。

#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); 
}

実際には、メソッドをチェーンする方法は奇妙なコードになり、助けになるのではなく可読性が損なわれます。自己ジャンクへの戻り参照を殺した場合、コードは次のようになります。

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

私は(私はそれをテストしていません)これはテンプレートを使用してそれを行うと思います:

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> { ... }

ノート:

  • 任意/すべての基本クラスはテンプレートである必要があり、上部に別の非テンプレート リーフ クラスがあります (LabelTおよびLabelクラスと比較してください)。
  • キャストはお好みでdynamic_castどうぞ。
  • " " をキャストする代わりにreturn *this、基本クラスにデータ メンバー (派生クラスが基本クラスのコンストラクターにT&渡される) を含めることができます。this継承の代わりに、または継承と同様に。
于 2009-02-15T17:55:08.937 に答える
0

まあ、あなたはそれが であることを知っているので、返されたを としてButtonキャストして続行できるはずです。しかし、それは少し醜く見えます。Widget&Button&

もう 1 つの厄介なオプションは、関数 (およびフレンド)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 に答える