c++ inheritance method-chaining

c++ - Método de encadenamiento+herencia no funcionan bien juntos?



inheritance method-chaining (15)

Considerar:

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

¿Alguna técnica para evitar estos problemas?

Ejemplo: usar magia de plantilla para hacer que Button :: move return Button y algo.

editar Se ha vuelto claro que el segundo problema se resuelve haciendo que setAngle sea virtual.

¡Pero el primer problema permanece sin resolver de una manera razonable!

editar : Bueno, supongo que es imposible hacerlo correctamente en C ++. Gracias por los esfuerzos de todos modos.


¿Es un Button realmente una Label ? Parece que estás violando el principio de sustitución de Liskov . Tal vez debería considerar el patrón Decorator para agregar comportamientos a Widgets.

Si insistes en la estructura tal como está, puedes resolver tu problema de la siguiente manera:

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


¿Realmente setText () y setAngle () necesitan devolver sus propios tipos en cada clase? Si los configura todos para que devuelvan Widget &, entonces puede usar las funciones virtuales de la siguiente manera:

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

Tenga en cuenta que incluso si el tipo de devolución es Widget &, las funciones de nivel Button o Label seguirán siendo las llamadas.


Bueno, sabes que es un Button así que deberías poder lanzar el Widget& devuelto Widget& como un Button& y continuar. Aunque se ve un poco feo.

Otra opción bastante molesta es crear un contenedor en su clase Button para la función Widget::move (y amigos). Sin embargo, probablemente no valga la pena el esfuerzo de envolver todo si tienes más que un puñado de funciones.


Creo (no lo he probado) esto lo hará usando plantillas:

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

Notas:

  • Cualquiera / cada clase base debe ser una plantilla, con una clase de hoja separada que no sea la plantilla en la parte superior (contraste las clases LabelT y Label ).
  • El elenco podría ser un dynamic_cast si quieres.
  • En lugar de lanzar el " return *this ", la clase base podría contener un T& como un miembro de datos (la clase derivada pasaría this al constructor de la clase base), que sería un miembro de datos adicional, pero que evita un reparto y Creo que puede permitir la composición en lugar de, o además de, la herencia.

Esto se compila en gcc 4.3.2 y es una especie de patrón 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); }


Para el segundo problema, hacer setAngle virtual debería hacer el truco.

Para el primero, no hay soluciones fáciles. Widget :: move devuelve un Widget, que no tiene un método setText. Podrías hacer un método de setText virtual puro, pero esa sería una solución bastante fea. Podría sobrecargar mover () en la clase de botón, pero sería una molestia mantener. Finalmente, probablemente puedas hacer algo con las plantillas. Quizás algo como esto:

// 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");

Te dejaré decidir qué solución es más limpia. ¿Pero hay alguna razón en particular por la que necesita poder encadenar estos métodos?


Una forma simple, pero molesta, de resolver su problema es volver a implementar todos sus métodos públicos en sus subclases. Esto no resuelve el problema con el polimorfismo (si lanzas desde Etiqueta a Widget, por ejemplo), que puede o no ser un problema importante.

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

Asegúrese de incluir en su documentación esto debe hacerse para encadenar al trabajo.

Pero realmente, ahora, ¿por qué molestarse con el método de encadenamiento? Muchas veces, estas funciones se llamarán de manera menos trivial y necesitarán líneas más largas. Esto realmente dañaría la legibilidad. Una acción por línea: esta es una regla general cuando se trata de ++ y también.


[despotricar]

Sí. Salga de este método encadenando negocios y simplemente llame a funciones en una fila.

En serio, pagas un precio por permitir esta sintaxis y no obtengo los beneficios que ofrece.

[/despotricar]


C ++ admite la covarianza de valor de retorno en métodos virtuales. Entonces puedes obtener algo como lo que quieres con un poco de trabajo:

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

Tenga en cuenta que debe usar métodos virtuales para hacer algo como esto. Si no son métodos virtuales, los métodos ''reimplementados'' darán como resultado la ocultación de nombres y el método llamado dependerá del tipo estático de la variable, puntero o referencia, por lo que podría no ser el método correcto si un puntero base o referencia está siendo utilizada.


Durante un tiempo pensé que sería posible sobrecargar el operator->() poco habitual operator->() para métodos de encadenamiento en lugar de . , pero eso vaciló porque parece que el compilador requiere el identificador a la derecha de -> para pertenecer al tipo estático de la expresión de la izquierda. Lo suficientemente justo.

El método del hombre pobre encadenamiento

Dando un paso atrás por un momento, el punto de la cadena de métodos es evitar escribir nombres de objetos largos repetidamente. Sugeriré el siguiente enfoque rápido y sucio:

En lugar de la "forma a mano":

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

Puedes escribir:

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

No, no es tan conciso como encadenamiento real . , pero ahorrará algo de tipeo cuando haya muchos parámetros para establecer, y tiene el beneficio de que no requiere cambios de código en sus clases existentes. Debido a que envuelve todo el grupo de llamadas a métodos en {} para limitar el alcance de la referencia, siempre puede usar el mismo identificador corto (por ejemplo _ o x ) para representar el nombre del objeto particular, lo que aumenta potencialmente la legibilidad. Finalmente, el compilador no tendrá problemas para optimizar el proceso _ .


No con C ++.

C ++ no admite la varianza en los tipos de devolución, por lo que no hay forma de cambiar el tipo estático de la referencia devuelta por Widget.move () para que sea más específico que Widget e incluso si lo reemplaza.

El C ++ necesita poder verificar las cosas en tiempo de compilación, por lo que no puede usar el hecho de que lo que realmente se devuelve del movimiento es un botón.

En el mejor de los casos, puedes hacer algunos castings en tiempo de ejecución, pero no va a verse bonito. Solo llamadas separadas.

Editar : Sí, soy muy consciente del hecho de que el estándar de C ++ dice que la covarianza de valor de retorno es legítima. Sin embargo, en el momento en que estaba enseñando y practicando C ++, algunos compiladores convencionales (por ejemplo, VC ++) no lo hicieron. Por lo tanto, para la portabilidad recomendamos no hacerlo. Es posible que los compiladores actuales no tengan ningún problema con eso, finalmente.


Puede extender CRTP para manejar esto. La solución de Monjardin va en la dirección correcta. Todo lo que necesita ahora es una implementación de Label predeterminada para usarla como clase de hoja.

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

Dicho esto, considere si tal esfuerzo vale la pequeña bonificación de sintaxis añadida. Considere soluciones alternativas.


Otras personas han tenido problemas de diseño. Puede solucionar el problema en cierta forma (aunque de una manera bastante burda) utilizando el soporte de C ++ para los tipos de retorno covariantes.

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

Realmente, aunque los métodos de encadenamiento de la forma que eres hacen código extraño, y perjudica la legibilidad en lugar de ayuda. Si mataste la referencia de devolución a la basura propia, tu código se convertiría en:

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


Yo abandonaría las cosas del encadenamiento. Por un lado, en realidad no se puede hacer sin hacer algunos hacks relativamente desagradables. Pero, el mayor problema es que hace más difícil leer y mantener el código y lo más probable es que la gente abuse de él, creando una línea de código gigante para hacer varias cosas (piense en el álgebra de la escuela secundaria y esas líneas gigantescas). de adiciones, restas y multiplicaciones en las que siempre parece terminar en algún momento, eso es lo que las personas harán si las deja).

Otro problema es que debido a que la mayoría de sus funciones en el sistema van a devolver una referencia a sí mismo, va a ser lógico que todas ellas lo hagan. Cuando ( no si ) finalmente comienzas a implementar funciones que también deben devolver valores (no solo los descriptores de acceso, sino también algunos mutadores, y otras funciones genéricas), te enfrentarás a un dilema, ya sea romper tu convención (lo que hará bola de nieve, por lo que no está claro cómo deberían implementarse las cosas para otras funciones futuras) o verse obligado a comenzar a devolver valores a través de parámetros (que estoy seguro de que detestaría, como la mayoría de los otros programadores que conozco también).


Realmente, el encadenamiento de métodos es una mala idea debido a la poca seguridad de las excepciones. ¿Realmente lo necesitas?