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


Entiendo la mecánica de polimorfismo estático utilizando el Patrón de plantilla curiosamente recurrente. Simplemente no entiendo para qué es bueno.

La motivación declarada es:

Sacrificamos cierta flexibilidad de polimorfismo dinámico para velocidad.

Pero por qué molestarse con algo tan complicado me gusta:

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

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

Cuando puedes hacer:

class Base
{
public: 
    void interface();
}

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

Mi mejor suposición es que no hay diferencia semántica en el código y que solo se trata de un buen estilo de C ++.

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

Prefiere hacer que las funciones virtuales sean privadas.

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

En el contexto de esta guía, el primer ejemplo es bueno, porque:

los void implementation() La función 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 interferir con la interfaz pública para realizar la personalización.

Mi pregunta es:

  1. ¿Qué me estoy perdiendo sobre el polimorfismo estático? ¿Se trata de un buen estilo de C ++?
  2. ¿Cuándo debería usarse? ¿Cuáles son algunas pautas?

32
2017-09-28 02:59


origen


Respuestas:


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

El polimorfismo estático y el polimorfismo en tiempo de ejecución son cosas diferentes y logran diferentes objetivos. Ambos son técnicamente polimorfismo, en el sentido de que deciden qué pieza de código ejecutar en función del tipo de algo. El polimorfismo en tiempo de ejecución difiere vinculando el tipo de algo (y, por tanto, el código que se ejecuta) hasta el tiempo de ejecución, mientras que el polimorfismo estático se resuelve por completo en tiempo de compilación.

Esto resulta en pros y contras para cada uno. Por ejemplo, el polimorfismo estático puede verificar las suposiciones en tiempo de compilación, o seleccionar entre las opciones que no compilarían de otra manera. También proporciona una gran cantidad de información para el compilador y optimizador, que puede alinearse sabiendo completamente el objetivo 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 dar como resultado una saturación de tamaño de código binario (las plantillas son sofisticadas, copiar, pegar) y no permite que estas determinaciones ocurran en tiempo de ejecución.

Por ejemplo, considere algo así 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 forma de que esto se compile usando el polimorfismo en tiempo de ejecución. Tienes que tomar la decisión en tiempo de compilación. (Por lo general, harías esto con el envío de etiquetas, 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 se conoce el tipo en el momento de la compilación. Considerar:

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

Aquí, DoAndLog no sabe nada sobre el real ostream implementación que se obtiene, y puede ser imposible determinar de forma estática qué tipo se pasará. Claro, esto se puede convertir en una plantilla:

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

Pero estas fuerzas DoAndLog para ser implementado en un archivo de encabezado, que puede ser poco práctico. También requiere que todas las implementaciones posibles de StreamTson visibles en tiempo de compilación, lo que puede no ser cierto: el polimorfismo en tiempo de ejecución puede funcionar (aunque esto no se recomienda) a través de los límites DLL o SO.


¿Cuándo debería usarse? ¿Cuáles son algunas pautas?

Esto es como alguien que viene hacia usted y le dice "cuando estoy escribiendo una oración, ¿debería usar oraciones compuestas u oraciones simples"? O tal vez un pintor que dice: "¿Debería usar siempre pintura roja o azul?" No hay una respuesta correcta, y no hay un conjunto de reglas que puedan seguirse ciegamente aquí. Tienes que ver los pros y los contras de cada enfoque, y decidir qué mejores mapas para tu dominio problemático en particular.


En cuanto a la CRTP, la mayoría de los casos de uso para eso son para permitir que la clase base proporcione algo en términos de la clase derivada; p.ej. Boost's iterator_facade. La clase base necesita tener cosas como DerivedClass operator++() { /* Increment and return *this */ } inside: especificado en términos de derivada en la función miembro firmas.

Se puede usar con fines polimórficos, pero no he visto demasiados.


36
2017-09-28 03:30



El enlace que proporciona menciona impulsa iteradores como un ejemplo de polimorfismo estático. Los iteradores STL también exhiben 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 polimorfismo 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 patrón de plantilla curiosamente recurrente interviene para ayudarnos a generar el tipo de iterador. Aquí hay una aproximación aproximada de la clase de 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 podría proporcionar la implementación anterior!

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

Creo que vale la pena echar un vistazo al origen de toda esta complejidad, y por qué los lenguajes como Java y C # intentan sobre todo evitarlo: ¡escribe borrado! En c ++ no hay nada útil que contenga Object escriba con información útil. En cambio, tenemos void* y una vez que tengas void* ¡Realmente no tienes nada! Si tiene una interfaz que se descompone en void* la única manera de recuperarse es haciendo suposiciones peligrosas o manteniendo información de tipo extra.


3
2017-09-28 03:46



Si bien puede haber casos en los que el polimorfismo estático es útil (las otras respuestas se enumeran algunas), generalmente lo veo como algo malo. ¿Por qué? Como ya no puede utilizar 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 a objetos.

La diferencia de tiempo de ejecución entre el polimorfismo estático y el dinámico es exactamente dos desregulaciones del puntero (si el compilador realmente indica el método de envío en la clase base, si por alguna razón no lo hace, el polimorfismo estático es más lento). Eso no es realmente caro, especialmente debido a que la segunda búsqueda casi siempre debe presionar el caché. En general, esas búsquedas son generalmente más baratas que la función llamada en sí misma, y ​​sin duda vale la pena para obtener la flexibilidad real proporcionada por el polimorfismo dinámico.


1
2017-09-28 07:29