¿Cuál es la motivación detrás del polimorfismo estático en C++?


Entiendo la mecánica del polimorfismo estáticousando el Curiosamente Recurrente Patrón de Plantilla. Simplemente no entiendo para qué sirve.

La motivación declarada es:

Sacrificamos cierta flexibilidad de polimorfismo dinámico para velocidad.

Pero ¿por qué molestarse con algo tan complicado como:

template <class Derived>
class Base
{
public:
    void interface()
    {
         // ...
         static_cast<Derived*>(this)->implementation();
         // ...
    }
};

class Derived : Base<Derived>
{
private:
     void implementation();
};

Cuando solo puedes hacer:

class Base
{
public: 
    void interface();
}

class Derived : public Base
{
public: 
    void interface();
}

Mi mejor conjetura es que no hay semántica diferencia en el código y que es solo una cuestión de buen estilo C++.

Herb Sutter escribió en Exceptional C++ style: Chapter 18 que:

Prefieren hacer privadas las funciones virtuales.

Acompañado, por supuesto, con una explicación exhaustiva de por qué esto es buen estilo.

En el contexto de esta directriz, el primer ejemplo es bueno , porque:

La función void implementation() en el ejemplo puede pretender ser virtual, ya que está aquí para realizar la personalización de la clase. Por lo tanto, debe ser privado.

Y el segundo ejemplo es malo , ya que:

No debemos entrometernos con la interfaz pública para realizar la personalización.

Mi pregunta es:

  1. ¿Qué me estoy perdiendo del polimorfismo estático? ¿Se trata de un buen estilo C++?
  2. ¿Cuándo debe usarse? ¿Cuáles son algunas pautas?
Author: Martin Drozdik, 2013-09-28

3 answers

¿Qué me estoy perdiendo del polimorfismo estático? ¿Se trata de un buen estilo C++?

El polimorfismo estático y el polimorfismo en tiempo de ejecución son cosas diferentes y logran objetivos diferentes. Ambos son técnicamente polimorfismo, ya que deciden qué pieza de código ejecutar en función del tipo de algo. Runtime polymorphism aplaza el enlace del tipo de algo (y por lo tanto el código que se ejecuta) hasta el tiempo de ejecución, mientras que static polymorphism se resuelve completamente en compile tiempo.

Esto resulta en pros y contras para cada uno. Por ejemplo, el polimorfismo estático puede verificar suposiciones en tiempo de compilación, o seleccionar entre opciones que no compilarían de otra manera. También proporciona toneladas de información al compilador y optimizador, que puede en línea conocer completamente el destino de las llamadas y otra información. Pero el polimorfismo estático requiere que las implementaciones estén disponibles para que el compilador las inspeccione en cada unidad de traducción, puede resultar en un tamaño de código binario hinchado (las plantillas son fancy pants copy paste), y no permiten que estas determinaciones ocurran en tiempo de ejecución.

Por ejemplo, considere algo como std::advance:

template<typename Iterator>
void advance(Iterator& it, ptrdiff_t offset)
{
    // If it is a random access iterator:
    // it += offset;
    // If it is a bidirectional iterator:
    // for (; offset < 0; ++offset) --it;
    // for (; offset > 0; --offset) ++it;
    // Otherwise:
    // for (; offset > 0; --offset) ++it;
}

No hay manera de hacer que esto se compile usando el polimorfismo en tiempo de ejecución. Tienes que tomar la decisión en tiempo de compilación. (Normalmente lo haría con tag dispatch, por ejemplo)

template<typename Iterator>
void advance_impl(Iterator& it, ptrdiff_t offset, random_access_iterator_tag)
{
    // Won't compile for bidirectional iterators!
    it += offset;
}

template<typename Iterator>
void advance_impl(Iterator& it, ptrdiff_t offset, bidirectional_iterator_tag)
{
    // Works for random access, but slow
    for (; offset < 0; ++offset) --it; // Won't compile for forward iterators
    for (; offset > 0; --offset) ++it;
}

template<typename Iterator>
void advance_impl(Iterator& it, ptrdiff_t offset, forward_iterator_tag)
{
     // Doesn't allow negative indices! But works for forward iterators...
     for (; offset > 0; --offset) ++it;
}

template<typename Iterator>
void advance(Iterator& it, ptrdiff_t offset)
{
    // Use overloading to select the right one!
    advance_impl(it, offset, typename iterator_traits<Iterator>::iterator_category());
}  

Del mismo modo, hay casos en los que realmente no sabes el tipo en tiempo de compilación. Considere:

void DoAndLog(std::ostream& out, int parameter)
{
    out << "Logging!";
}

Aquí, DoAndLog no sabe cualquier cosa sobre la implementación real ostream que obtiene gets y puede ser imposible determinar estáticamente qué tipo se pasará. Claro, esto se puede convertir en una plantilla:

template<typename StreamT>
void DoAndLog(StreamT& out, int parameter)
{
    out << "Logging!";
}

Pero esto obliga a DoAndLog a implementarse en un archivo de cabecera, lo que puede ser poco práctico. También requiere que todas las implementaciones posibles de StreamT sean visibles en tiempo de compilación , lo que puede no ser cierto polym el polimorfismo en tiempo de ejecución puede funcionar (aunque esto no se recomienda) a través de DLL o límite.


¿Cuándo debe usarse? ¿Cuáles son algunas pautas?

Esto es como alguien que viene a ti y dice "cuando estoy escribiendo una oración, debo usar oraciones compuestas o oraciones simples"? O tal vez un pintor diciendo " ¿debería usar siempre pintura roja o pintura azul?"No hay una respuesta correcta, y no hay un conjunto de reglas que puedan seguirse ciegamente aquí. Usted tiene que mirar los pros y los contras de cada enfoque, y decidir qué mejores mapas a su particular dominio del problema.


En cuanto al CRTP, la mayoría de los casos de uso son para permitir que la clase base proporcione algo en términos de la clase derivada; por ejemplo, Boost iterator_facade. La clase base necesita tener cosas como DerivedClass operator++() { /* Increment and return *this */ } dentro de specified especificadas en términos de derivadas en la función miembro firmas.

Se puede usar para propósitos polimórficos, pero no he visto muchos de esos.

 37
Author: Billy ONeal,
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
2018-05-13 09:34:59

El enlace que proporcionas menciona los iteradores boost como un ejemplo de polimorfismo estático. Los iteradores STL también muestran este patrón. Echemos un vistazo a un ejemplo y consideremos por qué los autores de esos tipos decidieron que este patrón era apropiado:

#include <vector>
#include <iostream>
using namespace std;
void print_ints( vector<int> const& some_ints )
{
    for( vector<int>::const_iterator i = some_ints.begin(), end = some_ints.end(); i != end; ++i )
    {
        cout << *i;
    }
}

Ahora, ¿cómo implementaríamos int vector<int>::const_iterator::operator*() const; Podemos usar el polimprismo para esto? Bueno, no. ¿Cuál sería la firma de nuestra función virtual? void const* operator*() const? Eso es inútil! El tipo ha sido borrado (degradado de int a void*). En cambio, el curioso el patrón de plantilla recurrente interviene para ayudarnos a generar el tipo iterador. Aquí hay una aproximación aproximada de la clase iterador que necesitaríamos para implementar lo anterior:

template<typename T>
class const_iterator_base
{
public:
    const_iterator_base():{}

    T::contained_type const& operator*() const { return Ptr(); }
    T::contained_type const& operator->() const { return Ptr(); }
    // increment, decrement, etc, can be implemented and forwarded to T
    // ....
private:
    T::contained_type const* Ptr() const { return static_cast<T>(this)->Ptr(); }
};

El polimorfismo dinámico tradicional no pudo proporcionar la implementación anterior!

Un término relacionado e importante es polimorfismo paramétrico. Esto le permite implementar API similares en, por ejemplo, python que puede usar el patrón de plantilla curiosamente recurrente en C++. Espero que esto sea útil!

Creo vale la pena tomar una puñalada en la fuente de toda esta complejidad, y por qué lenguajes como Java y C# en su mayoría tratan de evitarlo: escriba borrado! En c++ no hay un all útil que contenga el tipo Object con información útil. En cambio, tenemos void* y una vez que tienes void* realmente no tienes nada! Si tiene una interfaz que decae a void*, la única manera de recuperarse es haciendo suposiciones peligrosas o manteniendo información de tipo adicional.

 4
Author: Dan O,
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
2013-09-28 03:58:52

Si bien puede haber casos en los que el polimorfismo estático es útil (las otras respuestas han enumerado algunas), generalmente lo vería como algo malo. ¿Por qué? Debido a que ya no puede usar un puntero a la clase base, siempre debe proporcionar un argumento de plantilla que proporcione el tipo derivado exacto. Y en ese caso, podrías usar el tipo derivado directamente. Y, para decirlo sin rodeos, el polimorfismo estático no es de lo que se trata la orientación del objeto.

La diferencia de tiempo de ejecución entre polimorfismo estático y dinámico hay exactamente dos desreferenciaciones de puntero (si el compilador realmente integra el método de envío en la clase base, si no lo hace por alguna razón, el polimorfismo estático es más lento). Eso no es realmente caro, especialmente porque la segunda búsqueda prácticamente siempre debe llegar a la caché. Con todo, esas búsquedas son generalmente más baratas que la función llamada en sí, y ciertamente valen la pena para obtener la flexibilidad real proporcionada por el polimorfismo dinámico.

 1
Author: cmaster,
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
2013-09-28 07:29:03