Evitar que el usuario derive de una base CRTP incorrecta


No puedo pensar en un título de pregunta apropiado para describir el problema. Esperemos que los detalles a continuación explica mi problema claro.

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

Compilará y ejecutará aunque la definición de D2 es intencionalmente incorrecta. La primera llamada d2.call_impl() producirá algunos bits aleatorios que se esperan ya que D2::data_ no se inicializó. La segunda y tercera llamadas saldrán todas 100 para el data_.

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

Cuando hacemos la llamada d2.call(), la llamada se resuelve a Base<D1>::call, y que va a lanzar this a D1 y llamar a D1::call_impl. Debido a que D1 es de hecho forma derivada Base<D1>, por lo que el cast está bien en tiempo de compilación.

En tiempo de ejecución, después del cast, this, mientras que 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 la salida. En este caso, estos bits estaban donde D2::data_ están. 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á ninguna señal 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 por defecto de la clase base o la clase derivada aplicación. Todo esto funcionará bien si la clase derivada definida por el usuario, por ejemplo D, se deriva de Base<D>. Generará un error de tiempo de compilación si se deriva de Base<Unrelated> donde Unrelated no se deriva de Base<Unrelated>. Pero no evitará que el usuario escriba código como el anterior.

El usuario usa la biblioteca derivando de la clase CRTP base y proporcionando algunos detalles de implementación. Hay ciertamente otras alternativas del diseño que pueden evitar el problema del uso incorrecto como antedicho (para ejemplo una clase base abstracta). Pero dejémoslos a un lado por ahora y créanme que necesito este diseño por alguna razón.

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

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

 24
Author: Mikhail, 2012-06-27

6 answers

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

2) declare el parámetro de plantilla derivado como amigo de la 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 ninguna instancia de la D2 heredada incorrecta.

 34
Author: user396672,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-06-28 08:49:32

Si tienes C++11 disponible, puedes usar static_assert (si no, estoy seguro de que puedes emular estas cosas con boost). Se podría afirmar para, por ejemplo, is_convertible<Derived*,Base*> o is_base_of<Base,Derived>.

Todo esto tiene lugar en Base, y todo lo que siempre tiene es la información Derivada. 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 Base<D1> se instanciauna vez, de una manera específica, sin importar si fue instanciado por D1 o D2 derivado de él (o por el usuario explícitamente instanciarlo).

Dado que no desea (comprensiblemente, ya que a veces tiene un costo de tiempo de ejecución significativo y sobrecarga de memoria) usar dynamic_cast, intente usar algo a menudo llamado "poly cast" (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 sus compilaciones de depuración/prueba, el error se detecta. Si bien no es una garantía del 100%, en la práctica esto a menudo captura todos los errores que comete la gente.

 4
Author: PlasmaHH,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-06-27 12:02:05

Punto general: Las plantillas no están protegidas de ser instanciadas con parámetros incorrectos. Este es un tema bien conocido. No se recomienda gastar tiempo en tratar de arreglar esto. El número o las formas en que se puede abusar de las plantillas es interminable. En tu caso particular podrías inventar algo. Más tarde modificarás tu código y aparecerán nuevas formas de abusar.

Sé que C++11 tiene una afirmación estática que podría ayudar. No conozco 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 asegurar que no haya recusrion en el código. Puede comprobar que no hay derivados de alguna clase, puede plantear restricciones en parámetros de plantillas y funciones, etc. Todo esto es análisis. El compilador no puede admitir restricciones tan variadas. No estoy seguro de que este sea el camino correcto a seguir, solo estoy hablando de esto posibilidad.

P. S. Nuestra empresa presta servicios en esta área.

 2
Author: Kirill Kobelev,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-06-27 11:56:10

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

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

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

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

    Plantilla static bool check (T * derived_this ) { return (derived_this = = Base:: derived( ) ); }

  3. En sus constructores Dn, call check( this ):

    Compruebe( 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]                                                                                                                                          
 1
Author: j4x,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-06-27 12:02:42

En general, no creo que haya una manera de conseguir esto, que no debe ser considerado abiertamente feo y revierte al uso de características malignas. Aquí hay un resumen de lo que funcionaría y lo que no.

  • El uso de static_assert (ya sea desde C++11 o desde 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 atrapará esto, ya que D1 actualmente se deriva de 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 derive de Base< D3 > tanto el static_assert como el static_cast activarán errores de compilación, por lo que esto es absolutamente inútil.

Dado que el tipo D2 que tendría que comprobar en el código de Base nunca se pasa a la plantilla la única manera de utilizar static_assert sería moverlo después de las declaraciones de D2 lo que requeriría que la misma persona que implementó D2 para comprobar, que de nuevo es inútil.

Una forma de evitar esto sería agregando una macro, pero esto no generaría nada más que fealdad pura:

#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 el static_assert es nuevamente superfluo, porque la plantilla se asegura de que los tipos coincidan siempre. 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 más a menudo probablemente tendría sentido implementar su propio asserted_cast (hay un artículo sobre el Dr. Jobbs sobre esto), que desencadena automáticamente una afirmación fallida cuando falla el dynamic_cast.
 1
Author: LiKao,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-06-27 12:12:32

No hay manera de evitar que el usuario escriba clases derivadas incorrectas; sin embargo, hay maneras de evitar que su código invoque clases con jerarquías inesperadas. Si hay puntos en los que el usuario está pasando Derived a las funciones de biblioteca, considere hacer que esas funciones de 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 ) compilará mientras que safe_call( d2 ) fallará al compilar. El error del compilador puede no ser tan explícito como uno quisiera para el usuario, por lo que puede valer la pena considerar afirmaciones estáticas.

 1
Author: Tanner Sansbury,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-06-27 13:00:58