funciones - plantillas c++
¿Puede una plantilla de función miembro de la clase C++ ser virtual? (11)
He oído que las plantillas de función miembro de la clase C ++ no pueden ser virtuales. ¿Es esto cierto?
Si pueden ser virtuales, ¿cuál es un ejemplo de un escenario en el que uno usaría esa función?
Tablas de funciones virtuales
Comencemos con algunos antecedentes sobre tablas de funciones virtuales y cómo funcionan ( source ):
[20.3] ¿Cuál es la diferencia entre cómo se llaman las funciones de miembro virtual y no virtual?
Las funciones de los miembros no virtuales se resuelven de forma estática. Es decir, la función miembro se selecciona estáticamente (en tiempo de compilación) en función del tipo de puntero (o referencia) al objeto.
En contraste, las funciones de los 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) según el tipo de objeto, no el tipo de puntero / referencia a ese objeto. Esto se llama "enlace dinámico". La mayoría de los compiladores usan alguna variante de la siguiente técnica: si el objeto tiene una o más funciones virtuales, el compilador 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 compilador 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 dibujar () y mover () y redimensionar (), habría exactamente una tabla v asociada con la clase Círculo, incluso si hubiera un círculo de objetos de Círculo, y el puntero v de cada uno de esos objetos Círculo apuntaría a la tabla v Círculo. La propia v-table tiene punteros a 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 puntero v del objeto a la tabla v de la clase, luego sigue la ranura correspondiente 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 adicional por objeto (pero solo para los objetos que necesitarán realizar un enlace dinámico), más un puntero adicional por método (pero solo para métodos virtuales). La sobrecarga de tiempo-costo también es bastante nominal: en comparación con una llamada de función normal, una llamada de función virtual requiere dos recuperaciones adicionales (una para obtener el valor del puntero v, una segunda 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 compilador resuelve funciones no virtuales exclusivamente en tiempo de compilació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 con plantilla que se implementarán de manera diferente para diferentes tipos de cubos (algunos almacenados por píxel, otros por imagen, etc.).
Algún código:
virtual void LoadCube(UtpBipCube<float> &Cube,long LowerLeftRow=0,long LowerLeftColumn=0,
long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
virtual void LoadCube(UtpBipCube<short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
virtual void LoadCube(UtpBipCube<unsigned short> &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 compilará debido a un combo virtual:
template<class T>
virtual void LoadCube(UtpBipCube<T> &Cube,long LowerLeftRow=0,long LowerLeftColumn=0,
long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
Terminé moviendo la declaración de plantilla al nivel de clase . Esta solución habría obligado a los programas 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<float> &Cube,long LowerLeftRow=0,long LowerLeftColumn=0,
long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
virtual void LoadCube(UtpBipCube<short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
virtual void LoadCube(UtpBipCube<unsigned short> &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 de niños.
void LoadCube(UtpBipCube<float> &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<short> &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<unsigned short> &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<class T>
void LoadAnyCube(UtpBipCube<T> &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 pila con una solución alternativa : necesita una solución de miembro de plantilla virtual .
Al menos con gcc 5.4, las funciones virtuales podrían ser miembros de plantilla, pero tienen que ser ellas mismas.
#include <iostream>
#include <string>
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 T> class mix: first , T {
public:
virtual std::string mixt() override;
};
template<class T> std::string mix<T>::mixt() {
return a1()+" before "+T::a2();
}
class mix2: public mix<last> {
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
C ++ no permite funciones de miembro de plantilla virtual en este momento. La razón más probable es la complejidad de su implementación. Rajendra da una buena razón por la que no se puede hacer en este momento, pero podría ser posible con cambios razonables de la norma. Especialmente, determinar cuántas instancias de una función de plantilla existe realmente y construir el vtable parece difícil si se considera el lugar de la llamada de la función virtual. La gente de estándares solo tiene muchas otras cosas que hacer en este momento y C ++ 1x también es mucho trabajo para los compiladores del compilador.
¿Cuándo necesitarías una función miembro con plantilla? Una vez me topé con una situación en la que traté de refactorizar una jerarquía con una clase base virtual pura. Era un estilo pobre para implementar diferentes estrategias. Quería cambiar el argumento de una de las funciones virtuales a un tipo numérico y en lugar de sobrecargar la función miembro y anular todas las sobrecargas en todas las subclases que intenté usar funciones de plantilla virtual (y tuve que descubrir que no existen .)
De Plantillas C ++ La Guía Completa:
Las plantillas de función miembro no pueden ser declaradas virtuales. Esta restricción se impone porque la implementación habitual del mecanismo de llamada a la función virtual utiliza una tabla de tamaño fijo con una entrada por función virtual. Sin embargo, el número de ejecuciones de una plantilla de función miembro no se fija hasta que se haya traducido todo el programa. Por lo tanto, el soporte de las plantillas de función miembro virtual requeriría soporte para un nuevo tipo de mecanismo en los compiladores y enlazadores de C ++. En contraste, los miembros ordinarios de las plantillas de clase pueden ser virtuales porque su número se corrige cuando se crea una instancia de una clase
El siguiente código se puede compilar y ejecutar correctamente, utilizando MinGW G ++ 3.4.5 en Windows 7:
#include <iostream>
#include <string>
using namespace std;
template <typename T>
class A{
public:
virtual void func1(const T& p)
{
cout<<"A:"<<p<<endl;
}
};
template <typename T>
class B
: public A<T>
{
public:
virtual void func1(const T& p)
{
cout<<"A<--B:"<<p<<endl;
}
};
int main(int argc, char** argv)
{
A<string> a;
B<int> b;
B<string> c;
A<string>* p = &a;
p->func1("A<string> a");
p = dynamic_cast<A<string>*>(&c);
p->func1("B<string> c");
B<int>* q = &b;
q->func1(3);
}
y la salida es:
A:A<string> a
A<--B:B<string> c
A<--B:3
Y luego agregué una nueva clase X:
class X
{
public:
template <typename T>
virtual void func2(const T& p)
{
cout<<"C:"<<p<<endl;
}
};
Cuando intenté usar la clase X en main () de esta manera:
X x;
x.func2<string>("X x");
g ++ informa del 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 miembro virtual se puede utilizar en una plantilla de clase. Es fácil para el compilador construir vtable
- Es imposible definir una función miembro de la plantilla de clase como virtual, como puede ver, es difícil determinar la firma de la función y asignar entradas de tabla.
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 plantillas para cada clase y una interfaz virtual común.
Sin embargo, es necesario definir para cada combinación de tipo de plantilla una función de contenedor virtual ficticia:
#include <memory>
#include <iostream>
#include <iomanip>
//---------------------------------------------
// 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 <typename T>
void getAreaT(T &area) {
area = static_cast<T>(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 <typename T>
void getAreaT(T &area) {
area = static_cast<T>(radius * radius * 3.1415926535897932385L);
}
};
//---------------------------------------------
// Main
int main()
{
// get area of square using template based function T=float
std::unique_ptr<Geometry> geometry = std::make_unique<Square>();
float areaSquare;
geometry->getArea(areaSquare);
// get area of circle using template based function T=long double
geometry = std::make_unique<Circle>();
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 del cuadrado es 1, el área del círculo es 3.1415926535897932385
Pruébalo here
Existe una solución alternativa para el "método de plantilla virtual" si se conoce de antemano el conjunto de tipos para el método de plantilla.
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 método de plantilla en implementaciones derivadas ( AImpl
, BImpl
) y utilizar Derived<*Impl>
.
class Base
{
public:
virtual ~Base()
{
}
template <typename T>
T Method(T t)
{
return VMethod(t);
}
private:
virtual int VMethod(int t) = 0;
virtual double VMethod(double t) = 0;
};
template <class Impl>
class Derived : public Impl
{
public:
template <class... TArgs>
Derived(TArgs&&... args)
: Impl(std::forward<TArgs>(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 <typename T>
T TMethod(T t)
{
return t - i;
}
private:
int i;
};
using A = Derived<AImpl>;
class BImpl : public Base
{
protected:
BImpl(int p)
: i(p)
{
}
template <typename T>
T TMethod(T t)
{
return t + i;
}
private:
int i;
};
using B = Derived<BImpl>;
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 un excedente para el código real ( VMethod
puede hacerse público y usarse directamente). Lo agregué para que se vea como un método de plantilla ''virtual'' real.
No, las funciones miembro de la plantilla no pueden ser virtuales.
No, no pueden. Pero:
template<typename T>
class Foo {
public:
template<typename P>
void f(const P& p) {
((T*)this)->f<P>(p);
}
};
class Bar : public Foo<Bar> {
public:
template<typename P>
void f(const P& p) {
std::cout << p << std::endl;
}
};
int main() {
Bar bar;
Bar *pbar = &bar;
pbar -> f(1);
Foo<Bar> *pfoo = &bar;
pfoo -> f(1);
};
tiene el mismo efecto si lo único que desea es tener una interfaz común y aplazar la implementación a las subclases.
Para contestar la segunda parte de la pregunta:
Si pueden ser virtuales, ¿cuál es un ejemplo de un escenario en el que uno usaría esa función?
Esto no es algo irrazonable para querer hacer. Por ejemplo, Java (donde cada método es virtual) no tiene problemas con los métodos genéricos.
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 miembro que acepta un objeto de función genérica.
La solución a este problema es usar el borrado de tipo con boost :: any_range y boost :: function, que le permitirá aceptar un iterador o funtor genérico sin la necesidad de convertir su función en una plantilla.
Las plantillas tienen que ver con el compilador que genera el código en tiempo de compilación . Las funciones virtuales se basan en 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 templada, la compilación se realiza y el compilador ya no puede generar la instancia adecuada. Por lo tanto, no puede tener plantillas de función miembro virtual.
Sin embargo, existen algunas técnicas poderosas e interesantes que se derivan de la combinación de polimorfismo y plantillas, en particular el llamado borrado de tipo .