¿Puede una plantilla de función miembro de clase C ++ ser virtual?

He oído que las plantillas de funciones de los miembros de la clase C ++ no pueden ser virtuales. ¿Es esto cierto?

Si pueden ser virtuales, ¿qué ejemplo hay de un escenario en el que uno usaría esa función?

Las plantillas tienen que ver con el código de generación del comstackdor en tiempo de comstackción . Las funciones virtuales tienen que ver con el sistema de tiempo de ejecución que determina a qué función llamar en tiempo de ejecución .

Una vez que el sistema de tiempo de ejecución descubrió que necesitaría llamar a una función virtual con plantilla, la comstackción ya está completa y el comstackdor ya no puede generar la instancia apropiada. Por lo tanto, no puede tener plantillas de funciones de miembros virtuales.

Sin embargo, hay algunas técnicas poderosas e interesantes que se derivan de la combinación de polymorphism y plantillas, especialmente el llamado borrado de tipos .

De las plantillas de C ++ La guía completa:

Las plantillas de funciones miembro no se pueden declarar virtuales. Esta restricción se impone porque la implementación habitual del mecanismo de llamada de función virtual utiliza una tabla de tamaño fijo con una entrada por función virtual. Sin embargo, el número de instancias de una plantilla de función miembro no se corrige hasta que se haya traducido todo el progtwig. Por lo tanto, el soporte de plantillas de funciones de miembros virtuales requeriría soporte para un tipo completamente nuevo de mecanismo en comstackdores y enlazadores de C ++. Por el contrario, los miembros ordinarios de las plantillas de clase pueden ser virtuales porque su número es fijo cuando se crea una instancia de una clase

C ++ no permite funciones de miembro de plantilla virtual en este momento. La razón más probable es la complejidad de implementarlo. Rajendra da una buena razón por la cual no se puede hacer ahora pero podría ser posible con cambios razonables del estándar. Especialmente averiguar cuántas instancias de una función con plantillas realmente existen y crear el vtable parece difícil si se considera el lugar de la llamada a la función virtual. Las personas de estándares simplemente tienen muchas otras cosas que hacer en este momento y C ++ 1x es mucho trabajo para los escritores de comstackción también.

¿Cuándo necesitarías una función de miembro con plantilla? Una vez me encontré con una situación en la que intenté refactorizar una jerarquía con una clase base virtual pura. Fue un estilo pobre para implementar diferentes estrategias. Quería cambiar el argumento de una de las funciones virtuales por un tipo numérico y, en lugar de sobrecargar la función miembro y anular todas las sobrecargas en todas las subclases, intenté usar funciones de plantillas virtuales (y tuve que descubrir que no existían) .)

Tablas de funciones virtuales

Comencemos con algunos antecedentes sobre tablas de funciones virtuales y cómo funcionan ( fuente ):

[20.3] ¿Cuál es la diferencia entre cómo se llaman funciones miembro virtuales y no virtuales?

Las funciones de miembros no virtuales se resuelven estáticamente. Es decir, la función miembro se selecciona estáticamente (en tiempo de comstackción) según el tipo de puntero (o referencia) del objeto.

Por el contrario, las funciones de miembros virtuales se resuelven dinámicamente (en tiempo de ejecución). Es decir, la función miembro se selecciona dinámicamente (en tiempo de ejecución) en función del tipo de objeto, no del tipo de puntero / referencia a ese objeto. Esto se llama “vinculación dinámica”. La mayoría de los comstackdores usan alguna variante de la siguiente técnica: si el objeto tiene una o más funciones virtuales, el comstackdor coloca un puntero oculto en el objeto llamado “puntero virtual” o “puntero v”. Este puntero v apunta a una tabla global llamada “tabla virtual” o “tabla v”.

El comstackdor crea una tabla v para cada clase que tiene al menos una función virtual. Por ejemplo, si la clase Círculo tiene funciones virtuales para draw () y move () y resize (), habría exactamente una tabla v asociada con la clase Circle, incluso si hubiera un montón de objetos Circle, y el v-pointer de cada uno de esos objetos de Círculo apuntaría a la tabla v del Círculo. La tabla v en sí tiene punteros para cada una de las funciones virtuales en la clase. Por ejemplo, la tabla v de Circle tendría tres punteros: un puntero a Circle :: draw (), un puntero a Circle :: move () y un puntero a Circle :: resize ().

Durante el envío de una función virtual, el sistema en tiempo de ejecución sigue el v-puntero del objeto a la tabla v de la clase, luego sigue el espacio apropiado en la tabla v al código del método.

La sobrecarga de costo de espacio de la técnica anterior es nominal: un puntero extra por objeto (pero solo para los objetos que necesitarán vinculación dinámica), más un puntero adicional por método (pero solo para los métodos virtuales). La sobrecarga de costo por tiempo también es bastante nominal: en comparación con una llamada a función normal, una llamada a función virtual requiere dos recuperaciones extra (una para obtener el valor del puntero v, y otra para obtener la dirección del método). Ninguna de estas actividades de tiempo de ejecución ocurre con funciones no virtuales, ya que el comstackdor resuelve funciones no virtuales exclusivamente en tiempo de comstackción en función del tipo de puntero.


Mi problema, o cómo vine aquí

Estoy intentando usar algo como esto ahora para una clase base de archivo de cubos con funciones de carga optimizadas que se implementarán de manera diferente para diferentes tipos de cubos (algunos almacenados por píxel, otros por imagen, etc.).

Cierto código:

 virtual void LoadCube(UtpBipCube &Cube,long LowerLeftRow=0,long LowerLeftColumn=0, long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0; virtual void LoadCube(UtpBipCube &Cube, long LowerLeftRow=0,long LowerLeftColumn=0, long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0; virtual void LoadCube(UtpBipCube &Cube, long LowerLeftRow=0,long LowerLeftColumn=0, long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0; 

Lo que me gustaría que fuera, pero no se comstackrá debido a un combo de plantillas virtuales:

 template virtual void LoadCube(UtpBipCube &Cube,long LowerLeftRow=0,long LowerLeftColumn=0, long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0; 

Terminé moviendo la statement de la plantilla al nivel de clase . Esta solución habría forzado a los progtwigs a conocer los tipos específicos de datos que leerían antes de leerlos, lo cual es inaceptable.

Solución

advertencia, esto no es muy bonito, pero me permitió eliminar el código de ejecución repetitiva

1) en la clase base

 virtual void LoadCube(UtpBipCube &Cube,long LowerLeftRow=0,long LowerLeftColumn=0, long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0; virtual void LoadCube(UtpBipCube &Cube, long LowerLeftRow=0,long LowerLeftColumn=0, long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0; virtual void LoadCube(UtpBipCube &Cube, long LowerLeftRow=0,long LowerLeftColumn=0, long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0; 

2) y en las clases para niños

 void LoadCube(UtpBipCube &Cube, long LowerLeftRow=0,long LowerLeftColumn=0, long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) { LoadAnyCube(Cube,LowerLeftRow,LowerLeftColumn,UpperRightRow,UpperRightColumn,LowerBand,UpperBand); } void LoadCube(UtpBipCube &Cube, long LowerLeftRow=0,long LowerLeftColumn=0, long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) { LoadAnyCube(Cube,LowerLeftRow,LowerLeftColumn,UpperRightRow,UpperRightColumn,LowerBand,UpperBand); } void LoadCube(UtpBipCube &Cube, long LowerLeftRow=0,long LowerLeftColumn=0, long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) { LoadAnyCube(Cube,LowerLeftRow,LowerLeftColumn,UpperRightRow,UpperRightColumn,LowerBand,UpperBand); } template void LoadAnyCube(UtpBipCube &Cube, long LowerLeftRow=0,long LowerLeftColumn=0, long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1); 

Tenga en cuenta que LoadAnyCube no está declarado en la clase base.


Aquí hay otra respuesta de desbordamiento de stack con una solución alternativa : necesita una solución de miembro de plantilla virtual .

El siguiente código se puede comstackr y ejecutar correctamente, utilizando MinGW G ++ 3.4.5 en la ventana 7:

 #include  #include  using namespace std; template  class A{ public: virtual void func1(const T& p) { cout< <"A:"< class B : public A { public: virtual void func1(const T& p) { cout< <"A<--B:"< a; B b; B c; A* p = &a; p->func1("A a"); p = dynamic_cast*>(&c); p->func1("B c"); B* q = &b; q->func1(3); } 

y el resultado es:

 A:A a A< --B:B c A< --B:3 

Y luego agregué una nueva clase X:

 class X { public: template  virtual void func2(const T& p) { cout< <"C:"< 

Cuando traté de usar la clase X en main () de esta manera:

 X x; x.func2("X x"); 

g ++ informa el siguiente error:

 vtempl.cpp:34: error: invalid use of `virtual' in template declaration of `virtu al void X::func2(const T&)' 

Entonces, es obvio que:

  • la función de miembro virtual se puede usar en una plantilla de clase. Es fácil para el comstackdor construir vtable
  • Es imposible definir una función de miembro de plantilla de clase como virtual, como puede ver, es difícil determinar la firma de función y asignar entradas de tabla variable.

No, no pueden. Pero:

 template class Foo { public: template void f(const P& p) { ((T*)this)->f

(p); } }; class Bar : public Foo { public: template void f(const P& p) { std::cout < < p << std::endl; } }; int main() { Bar bar; Bar *pbar = &bar; pbar -> f(1); Foo *pfoo = &bar; pfoo -> f(1); };

tiene el mismo efecto si lo único que desea hacer es tener una interfaz común y diferir la implementación de las subclases.

Para responder la segunda parte de la pregunta:

Si pueden ser virtuales, ¿qué ejemplo hay de un escenario en el que uno usaría esa función?

Esto no es algo irrazonable que quieras hacer. Por ejemplo, Java (donde cada método es virtual) no tiene problemas con los métodos generics.

Un ejemplo en C ++ de querer una plantilla de función virtual es una función miembro que acepta un iterador genérico. O una función de miembro que acepta un objeto de función genérico.

La solución a este problema es usar borrado de tipo con boost :: any_range y boost :: function, que le permitirá aceptar un iterador o un functor genérico sin la necesidad de hacer de su función una plantilla.

Existe una solución alternativa para el ‘método de plantilla virtual’ si el conjunto de tipos para el método de plantilla se conoce de antemano.

Para mostrar la idea, en el siguiente ejemplo solo se usan dos tipos ( int y double ).

Allí, un método de plantilla ‘virtual’ ( Base::Method ) llama al método virtual correspondiente (uno de Base::VMethod ) que, a su vez, llama a la implementación del método de plantilla ( Impl::TMethod ).

Solo se necesita implementar el método de plantilla TMethod en las implementaciones derivadas ( AImpl , BImpl ) y usar Derived< *Impl> .

 class Base { public: virtual ~Base() { } template  T Method(T t) { return VMethod(t); } private: virtual int VMethod(int t) = 0; virtual double VMethod(double t) = 0; }; template  class Derived : public Impl { public: template  Derived(TArgs&&... args) : Impl(std::forward(args)...) { } private: int VMethod(int t) final { return Impl::TMethod(t); } double VMethod(double t) final { return Impl::TMethod(t); } }; class AImpl : public Base { protected: AImpl(int p) : i(p) { } template  T TMethod(T t) { return t - i; } private: int i; }; using A = Derived; class BImpl : public Base { protected: BImpl(int p) : i(p) { } template  T TMethod(T t) { return t + i; } private: int i; }; using B = Derived; int main(int argc, const char* argv[]) { A a(1); B b(1); Base* base = nullptr; base = &a; std::cout < < base->Method(1) < < std::endl; std::cout << base->Method(2.0) < < std::endl; base = &b; std::cout << base->Method(1) < < std::endl; std::cout << base->Method(2.0) < < std::endl; } 

Salida:

 0 1 2 3 

NB: Base::Method es en realidad excedente para código real ( VMethod puede hacerse público y usarse directamente). Lo agregué para que parezca un método de plantilla "virtual" real.

No, las funciones de los miembros de la plantilla no pueden ser virtuales.

Al menos con gcc 5.4 las funciones virtuales podrían ser miembros de la plantilla, pero tienen que ser plantillas mismas.

 #include  #include  class first { protected: virtual std::string a1() { return "a1"; } virtual std::string mixt() { return a1(); } }; class last { protected: virtual std::string a2() { return "a2"; } }; template class mix: first , T { public: virtual std::string mixt() override; }; template std::string mix::mixt() { return a1()+" before "+T::a2(); } class mix2: public mix { virtual std::string a1() override { return "mix"; } }; int main() { std::cout < < mix2().mixt(); return 0; } 

Salidas

 mix before a2 Process finished with exit code 0 

En las otras respuestas, la función de plantilla propuesta es una fachada y no ofrece ningún beneficio práctico.

  • Las funciones de plantilla son útiles para escribir código solo una vez usando diferentes tipos.
  • Las funciones virtuales son útiles para tener una interfaz común para diferentes clases.

El lenguaje no permite funciones de plantillas virtuales, pero con una solución alternativa es posible tener ambas, por ejemplo, una implementación de plantilla para cada clase y una interfaz común virtual.

Sin embargo, es necesario definir para cada combinación de tipo de plantilla una función de envoltura virtual ficticia:

 #include  #include  #include  //--------------------------------------------- // Abstract class with virtual functions class Geometry { public: virtual void getArea(float &area) = 0; virtual void getArea(long double &area) = 0; }; //--------------------------------------------- // Square class Square : public Geometry { public: float size {1}; // virtual wrapper functions call template function for square virtual void getArea(float &area) { getAreaT(area); } virtual void getArea(long double &area) { getAreaT(area); } private: // Template function for squares template  void getAreaT(T &area) { area = static_cast(size * size); } }; //--------------------------------------------- // Circle class Circle : public Geometry { public: float radius {1}; // virtual wrapper functions call template function for circle virtual void getArea(float &area) { getAreaT(area); } virtual void getArea(long double &area) { getAreaT(area); } private: // Template function for Circles template  void getAreaT(T &area) { area = static_cast(radius * radius * 3.1415926535897932385L); } }; //--------------------------------------------- // Main int main() { // get area of square using template based function T=float std::unique_ptr geometry = std::make_unique(); float areaSquare; geometry->getArea(areaSquare); // get area of circle using template based function T=long double geometry = std::make_unique(); long double areaCircle; geometry->getArea(areaCircle); std::cout < < std::setprecision(20) << "Square area is " << areaSquare << ", Circle area is " << areaCircle << std::endl; return 0; } 

Salida:

El área cuadrada es 1, el área del círculo es 3.1415926535897932385

Pruébalo aquí