c++ crtp

c++ - Evitar que el usuario se derive de una base CRTP incorrecta



(6)

No puedo pensar en el título de una pregunta adecuada para describir el problema. Esperemos que los detalles a continuación expliquen mi problema claramente.

Considere el siguiente código

#include <iostream> template <typename Derived> class Base { public : void call () { static_cast<Derived *>(this)->call_impl(); } }; class D1 : public Base<D1> { public : void call_impl () { data_ = 100; std::cout << data_ << std::endl; } private : int data_; }; class D2 : public Base<D1> // This is wrong by intension { public : void call_impl () { std::cout << data_ << std::endl; } private : int data_; }; int main () { D2 d2; d2.call_impl(); d2.call(); d2.call_impl(); }

Se compilará y ejecutará aunque la definición de D2 sea ​​intencionalmente errónea. La primera llamada d2.call_impl() generará algunos bits aleatorios que se espera ya que D2::data_ no se inicializó. Las llamadas segunda y tercera serán todas de salida 100 para los data_ .

Entiendo por qué se compilará y ejecutará, corríjame si me equivoco.

Cuando hacemos la llamada d2.call() , la llamada se resuelve en Base<D1>::call , y eso se convertirá en D1 y llamará a D1::call_impl . Debido a que D1 se deriva de la forma Base<D1> , la conversión está bien en el momento de la compilación.

En el tiempo de ejecución, después de la conversión, this , mientras es realmente un objeto D2 se trata como si fuera D1 , y la llamada a D1::call_impl modificará los bits de memoria que se supone que son D1::data_ , y D1::data_ . En este caso, estos bits resultaron estar donde está D2::data_ . Creo que el segundo d2.call_impl() también será un comportamiento indefinido dependiendo de la implementación de C ++.

El punto es que este código, aunque intencionalmente incorrecto, no dará señales de error al usuario. Lo que realmente estoy haciendo en mi proyecto es que tengo una clase base CRTP que actúa como un motor de despacho. Otra clase en la biblioteca accede a la interfaz de la clase base CRTP, por ejemplo, call , y call se enviará a call_dispatch que puede ser la implementación predeterminada de la clase base o la implementación de la clase derivada. Todo esto funcionará bien si la clase derivada definida por el usuario, por ejemplo D , se deriva de Base<D> . Aumentará el error de tiempo de compilación si se deriva de Base<Unrelated> donde Unrelated no se deriva de Base<Unrelated> . Pero no impedirá al usuario escribir código como el anterior.

El usuario utiliza la biblioteca derivando de la clase CRTP base y proporcionando algunos detalles de implementación. Ciertamente, existen otras alternativas de diseño que pueden evitar el problema de uso incorrecto como el anterior (por ejemplo, una clase base abstracta). Pero dejémoslos de lado por ahora y solo créanme que necesito este diseño por alguna razón.

Así que mi pregunta es que, ¿hay alguna manera de evitar que el usuario escriba una clase derivada incorrecta como se ve arriba? Es decir, si el usuario escribe una clase de implementación derivada, digamos D , pero la derivó de la Base<OtherD> , se Base<OtherD> un error de tiempo de compilación.

Una solución es utilizar dynamic_cast . Sin embargo, eso es expansivo e incluso cuando funciona es un error en tiempo de ejecución.


1) hacer que todos los constructores de Base sean privados (si no hay constructores, agregue uno)

2) declarar parámetro de plantilla Derivado como amigo de Base

template <class Derived> class Base { private: Base(){}; // prevent undesirable inheritance making ctor private friend Derived; // allow inheritance for Derived public : void call () { static_cast<Derived *>(this)->call_impl(); } };

Después de esto, sería imposible crear cualquier instancia del D2 heredado incorrecto.


En general, no creo que haya una manera de obtener esto, que no debe considerarse completamente fea y se revierte al uso de características malvadas. Aquí hay un resumen de lo que funcionaría y lo que no.

  • El uso de static_assert (ya sea de C ++ 11 o de boost) no funciona, porque una comprobación en la definición de Base solo puede usar los tipos Base<Derived> y Derived . Así que lo siguiente se verá bien, pero fallará:

    template <typename Derived> class Base { public : void call () { static_assert( sizeof( Derived ) != 0 && std::is_base_of< Base< Derived >, Derived >::value, "Missuse of CRTP" ); static_cast<Derived *>(this)->call_impl(); } };

En caso de que intente declarar D2 como class D2 : Base< D1 > la afirmación estática no detectará esto, ya que D1 actualmente se deriva de la Base< D1 > y la afirmación estática es completamente válida. Sin embargo, si deriva de Base< D3 > donde D3 es cualquier clase que no se derive de Base< D3 > tanto static_assert como static_cast activarán errores de compilación, por lo que esto es absolutamente inútil.

Dado que el tipo D2 que necesitaría para verificar el código de Base nunca se pasa a la plantilla, la única forma de usar static_assert sería moverlo después de las declaraciones de D2 que requerirían la misma persona que implementó D2 para verificar, lo que nuevamente es inútil

Una forma de evitar esto sería mediante la adición de una macro, pero esto no engendraría más que pura fealdad:

#define MAKE_DISPATCHABLE_BEGIN( DeRiVeD ) / class DeRiVeD : Base< DeRiVed > { #define MAKE_DISPATCHABLE_END( DeRiVeD ) }; / static_assert( is_base_of< Base< Derived >, Derived >::value, "Error" );

Esto solo gana fealdad, y la static_assert es otra vez superflua, porque la plantilla se asegura de que los tipos siempre coincidan. Así que no hay ganancia aquí.

  • La mejor opción: olvídate de todo esto y usa dynamic_cast que fue explícitamente pensado para este escenario. Si necesita esto con más frecuencia, probablemente tenga sentido implementar su propio asserted_cast (hay un artículo sobre el Dr. Jobbs en este tema), que activa automáticamente una dynamic_cast fallida cuando falla el dynamic_cast .

No hay forma de evitar que el usuario escriba clases derivadas incorrectas; sin embargo, hay formas de evitar que su código invoque clases con jerarquías inesperadas. Si hay puntos en los que el usuario pasa Derived a las funciones de la biblioteca, considere hacer que esas funciones de la biblioteca realicen un static_cast al tipo derivado esperado. Por ejemplo:

template < typename Derived > void safe_call( Derived& t ) { static_cast< Base< Derived >& >( t ).call(); }

O si hay múltiples niveles de jerarquía, considere lo siguiente:

template < typename Derived, typename BaseArg > void safe_call_helper( Derived& d, Base< BaseArg >& b ) { // Verify that Derived does inherit from BaseArg. static_cast< BaseArg& >( d ).call(); } template < typename T > void safe_call( T& t ) { safe_call_helper( t, t ); }

En ambos casos, safe_call( d1 ) se compilará, mientras que safe_call( d2 ) no se compilará. El error del compilador puede no ser tan explícito como a uno le gustaría al usuario, por lo que puede valer la pena considerar afirmaciones estáticas.


Punto general: las plantillas no están protegidas contra la creación de instancias con parámetros incorrectos. Este es un problema bien conocido. No se recomienda dedicar tiempo a intentar solucionar este problema. El número o las formas en que se pueden abusar de las plantillas es infinito. En tu caso particular podrías inventar algo. Más adelante, modificarás tu código y aparecerán nuevas formas de abuso.

Sé que C ++ 11 tiene una afirmación estática que podría ayudar. No sé todos los detalles.

Otro punto Además de compilar errores hay análisis estático. Lo que estás pidiendo tiene algo con esto. El análisis no busca necesariamente fallas de seguridad. Puede garantizar que no haya ninguna recusrion en el código. Puede verificar que no haya derivados de alguna clase, puede imponer restricciones en los parámetros de plantillas y funciones, etc. Esto es todo análisis. Estas restricciones que varían ampliamente no pueden ser soportadas por el compilador. No estoy seguro de que este sea el camino correcto, solo de contarle esta posibilidad.

PS Nuestra empresa presta servicios en esta área.


Si no puedes contar con C ++ 11, puedes probar este truco:

  1. Agregue una función estática en Base que devuelva un puntero a su tipo especializado:

    Derivado estático * derivado () {return NULL; }

  2. Agregue una plantilla de función de check estática a la base que toma un puntero:

    template <typename T> static bool check (T * derivado_este) {return (derivado_este == Base <Derivado> :: derivado ()); }

  3. En sus constructores de Dn , llame a check( this ) :

    Mira esto )

Ahora si intentas compilar:

$ g++ -Wall check_inherit.cpp -o check_inherit check_inherit.cpp: In instantiation of ‘static bool Base<Derived>::check(T*) [with T = D2; Derived = D1]’: check_inherit.cpp:46:16: required from here check_inherit.cpp:19:62: error: comparison between distinct pointer types ‘D2*’ and ‘D1*’ lacks a cast check_inherit.cpp: In static member function ‘static bool Base<Derived>::check(T*) [with T = D2; Derived = D1]’: check_inherit.cpp:20:5: warning: control reaches end of non-void function [-Wreturn-type]


Si tiene C ++ 11 disponible, puede usar static_assert (de lo contrario, estoy seguro de que puede emular estas cosas con impulso). Podría hacer valer para, por ejemplo, is_convertible<Derived*,Base*> o is_base_of<Base,Derived> .

Todo esto tiene lugar en la Base, y todo lo que tiene es la información de Derivado. Nunca tendrá la oportunidad de ver si el contexto de llamada es de un D2 o D1, ya que esto no hace ninguna diferencia, ya que la Base<D1> se crea una instancia de una manera específica, sin importar si fue una instanciada por D1 o D2. desde él (o por el usuario que lo crea explícitamente).

Dado que no desea (comprensiblemente, dado que a veces tiene un costo de tiempo de ejecución significativo y una sobrecarga de memoria) use dynamic_cast, intente usar algo que a menudo se denomina "conversión de polietileno" (boost también tiene su propia variante):

template<class R, class T> R poly_cast( T& t ) { #ifndef NDEBUG (void)dynamic_cast<R>(t); #endif return static_cast<R>(t); }

De esta manera, en tu debug / test construye el error que se detecta. Si bien no es una garantía del 100%, en la práctica esto a menudo atrapa todos los errores que las personas cometen.