Цепочка методов + наследование не очень хорошо сочетаются друг с другом? - PullRequest
14 голосов
/ 15 февраля 2009

Рассмотрим:

// 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 return Button & или что-то в этом роде.

edit Стало ясно, что вторая проблема решается путем создания виртуального setAngle.

Но первая проблема остается неразрешенной разумным образом!

edit : Ну, я думаю, это невозможно сделать правильно в C ++. В любом случае, спасибо за усилия.

Ответы [ 15 ]

14 голосов
/ 21 февраля 2009

Вы можете расширить CRTP , чтобы справиться с этим. Решение Монжардина идет в правильном направлении. Все, что вам сейчас нужно - это реализация по умолчанию 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();
}

Тем не менее, подумайте, стоит ли такое усилие небольшого дополнительного синтаксического бонуса. Рассмотрим альтернативные решения.

7 голосов
/ 15 февраля 2009

Для второй проблемы, виртуализация setAngle должна помочь.

Для первого нет простых решений. Widget :: move возвращает виджет, который не имеет метода 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");

Я дам вам решить, какое решение является самым чистым. Но есть ли какая-то конкретная причина, по которой вам нужно связать эти методы?

4 голосов
/ 15 февраля 2009

Является ли Button действительно Label? Вы, кажется, нарушаете принцип замены Лискова . Возможно, вам следует рассмотреть шаблон Decorator для добавления поведения в виджеты.

Если вы настаиваете на структуре как есть, вы можете решить свою проблему следующим образом:

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); 
}
4 голосов
/ 15 февраля 2009

Простой, но раздражающий способ решения вашей проблемы - переопределить все ваши публичные методы в ваших подклассах. Это не решает проблему с полиморфизмом (например, если вы преобразуете из 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; }
};

Обязательно включите в свою документацию это необходимо сделать для работы цепочки.

Но на самом деле, зачем теперь цепляться за метод? Во многих случаях эти функции будут вызываться не так просто, и им понадобятся более длинные строки. Это действительно повредит читабельности. Одно действие на строку - это общее правило, когда речь идет о ++ и -- тоже.

3 голосов
/ 16 февраля 2009

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

Обратите внимание, что вы должны использовать виртуальные методы для чего-то подобного. Если они не являются виртуальными методами, то «переопределенные» методы приведут к сокрытию имени, а вызываемый метод будет зависеть от статического типа переменной, указателя или ссылки, поэтому это может быть неправильный метод, если указатель базы или ссылка используется.

2 голосов
/ 25 февраля 2009

Я бы отказался от цепочки. С одной стороны, это не может быть сделано без каких-либо относительно неприятных хаков. Но самая большая проблема заключается в том, что это усложняет чтение и поддержку кода, и вы, скорее всего, в конечном итоге получите людей, злоупотребляющих им, создающих одну гигантскую строку кода для выполнения нескольких задач (вспомните алгебру средней школы и эти гигантские строки сложений, вычитаний и умножений, которые, как вам кажется, в какой-то момент заканчиваются, это то, что люди будут делать, если вы позволите им).

Другая проблема состоит в том, что, поскольку большинство ваших функций в системе будут возвращать ссылку на себя, логично, что все они должны это делать. Когда ( not if ) вы, наконец, начинаете реализовывать функции, которые также должны возвращать значения (не только методы доступа, но также будут некоторые мутаторы и другие универсальные функции), вы также столкнетесь с дилеммой: нарушить ваше соглашение (что приведет к снежному кому, делая неясным, как все должно быть реализовано для других будущих функций) или будет вынуждено начать возвращать значения через параметры (что, я уверен, вам не понравится, как и большинство других моих знакомых программистов) хорошо).

2 голосов
/ 16 февраля 2009

Какое-то время я думал, что можно было бы перегрузить немного необычный operator->() для методов цепочки вместо ., но это дало сбой, потому что кажется, что компилятору требуется идентификатор справа от -> принадлежать к типу выражения static слева. Достаточно справедливо.

Метод бедняков

Возвращаясь на мгновение, смысл цепочки методов состоит в том, чтобы избежать многократного ввода длинных имен объектов. Я предложу следующий быстрый и грязный подход:

Вместо "длинной формы":

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

Вы можете написать:

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

Нет, это не так кратко, как реальное связывание с ., но оно сохранит некоторую типизацию, когда нужно задать много параметров, и имеет то преимущество, что требует без изменения кода на ваши существующие классы. Поскольку вы ограничиваете всю группу вызовов методов в {}, чтобы ограничить область действия ссылки, вы всегда можете использовать один и тот же короткий идентификатор (например, _ или x) для обозначения конкретного имени объекта, потенциально повышая читабельность , Наконец, у компилятора не возникнет проблем при оптимизации _.

2 голосов
/ 15 февраля 2009

Не с C ++.

C ++ не поддерживает дисперсию в возвращаемых типах, поэтому нет способа изменить статический тип ссылки, возвращаемой из Widget.move (), на более конкретную, чем Widget, даже если вы переопределите ее.

C ++ должен иметь возможность проверять вещи во время компиляции, так что вы не можете использовать тот факт, что то, что действительно возвращается после перемещения, это кнопка.

В лучшем случае вы можете выполнить кастинг во время выполнения, но это не будет выглядеть красиво. Просто отдельные звонки.

Редактировать : Да, я хорошо осознаю тот факт, что стандарт C ++ говорит, что ковариация возвращаемых значений является законной. Однако в то время, когда я преподавал и практиковал C ++, некоторые основные компиляторы (например, VC ++) этого не делали. Следовательно, для мобильности мы рекомендуем против этого. Вполне возможно, что современные компиляторы не имеют проблем с этим, наконец.

2 голосов
/ 15 февраля 2009

[декламация]

Да. Выйдите из этого метода, связанного с бизнесом, и просто вызовите функции подряд.

Серьезно, вы платите цену за использование этого синтаксиса, а я не получаю преимуществ, которые он предлагает.

[/ декламация]

1 голос
/ 15 февраля 2009

Это компилируется в gcc 4.3.2 и является своего рода mixin pattern .

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