He oído que las plantillas de funciones miembro de 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 tal función?
He oído que las plantillas de funciones miembro de 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 tal función?
Respuestas:
Las plantillas tienen que ver con el código de generación del compilador en tiempo de compilación . Las funciones virtuales tienen que ver con el sistema de tiempo de ejecución para determinar 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 compilación está lista y el compilador ya no puede generar la instancia adecuada. Por lo tanto, no puede tener plantillas de funciones miembro virtuales.
Sin embargo, existen algunas técnicas poderosas e interesantes derivadas de la combinación de polimorfismo y plantillas, en particular el llamado borrado de tipo .
Virtual functions are all about the run-time system figuring out which function to call at run-time
- Lo siento, pero esta es una forma bastante incorrecta, y bastante confuso. Es solo indirección, y no hay un "tiempo de ejecución que se resuelva", se sabe durante el tiempo de compilación que la función a llamar es la señalada por el enésimo puntero en la tabla. "Resolver" implica que hay verificaciones de tipo y tal, que no es el caso. Once the run-time system figured out it would need to call a templatized virtual function
- Se sabe si la función es virtual o no en tiempo de compilación.
void f(concr_base& cb, virt_base& vb) { cb.f(); vb.f(); }
, entonces "sabe" qué función se invoca en el punto cb.f()
llamado, y no lo sabe para vb.f()
. Este último tiene que ser encontrado en tiempo de ejecución , por el sistema de tiempo de ejecución . Si desea llamar a esto "descifrar" y si esto es más o menos eficiente, no cambia un poco estos hechos.
Desde plantillas de C ++ La guía completa:
Las plantillas de funciones miembro no pueden declararse 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 fija hasta que se haya traducido todo el programa. Por lo tanto, el soporte de plantillas de funciones miembro virtuales requeriría soporte para un nuevo tipo de mecanismo en compiladores y enlazadores de C ++. Por el contrario, los miembros ordinarios de las plantillas de clase pueden ser virtuales porque su número se fija cuando se instancia 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 que no se puede hacer en este momento, pero podría ser posible con cambios razonables del estándar. Especialmente determinar cuántas instancias de una función con plantilla existen realmente y construir la tabla virtual parece difícil si considera el lugar de la llamada a 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 redactores del compilador.
¿Cuándo necesitarías una función 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. 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 cada sobrecarga en todas las subclases, intenté usar funciones de plantilla virtual (y tuve que descubrir que no existen .)
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 las funciones miembro virtuales y no virtuales?
Las funciones miembro no virtuales se resuelven estáticamente. 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 miembro 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 en v apunta a una tabla global llamada "tabla virtual" o "tabla en v".
El compilador crea una tabla v para cada clase que tiene al menos una función virtual. Por ejemplo, si la clase Circle tiene funciones virtuales para dibujar () y mover () y cambiar el tamaño (), habría exactamente una tabla v asociada con la clase Circle, incluso si hubiera un billón de objetos Circle, y el puntero v de cada uno de esos objetos Circle apuntaría a la v-table Circle. La tabla v en sí tiene punteros a cada una de las funciones virtuales de la clase. Por ejemplo, la v-table 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 de 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 del costo de espacio de la técnica anterior es nominal: un puntero adicional por objeto (pero solo para 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 costo de tiempo 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 en V, una segunda para obtener la dirección del método). Nada de esta actividad 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.
Estoy intentando usar algo como esto ahora para una clase base de cubefile con funciones de carga optimizadas con plantillas 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 con plantilla:
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.
advertencia, esto no es muy bonito pero me permitió eliminar el código de ejecución repetitivo
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 infantiles
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 se declara 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 .
El siguiente código puede compilarse y ejecutarse 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 () así:
X x;
x.func2<string>("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:
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 todo lo que quiere hacer es tener una interfaz común y diferir la implementación a las subclases.
Foo
puntero está calificado como Foo<Bar>
, no puede apuntar a un Foo<Barf>
o Foo<XXX>
.
No, las funciones miembro de la plantilla no pueden ser virtuales.
En las otras respuestas, la función de plantilla propuesta es una fachada y no ofrece ningún beneficio práctico.
El lenguaje no permite funciones de plantilla virtual, 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 contenedor virtual ficticio:
#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 cuadrada es 1, el área del círculo es 3.1415926535897932385
Pruébalo aquí
Para responder a la segunda parte de la pregunta:
Si pueden ser virtuales, ¿cuál es un ejemplo de un escenario en el que uno usaría tal función?
Esto no es una cosa irrazonable 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 acepte un iterador genérico. O una función miembro que acepta un objeto de función genérico.
La solución a este problema es utilizar el borrado de tipo con la función boost :: any_range y boost ::, que le permitirá aceptar un iterador genérico o un functor sin la necesidad de convertir su función en una plantilla.
Existe una solución alternativa para el 'método de plantilla virtual' si se conoce de antemano un conjunto de tipos para el método de plantilla.
Para mostrar la idea, en el ejemplo a continuación 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 implementaciones derivadas ( AImpl
, BImpl
) y usar 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: en
Base::Method
realidad es excedente para el código real ( VMethod
puede hacerse público y usarse directamente). Lo agregué para que parezca un método de plantilla 'virtual' real.
Base
clase original cada vez que necesita llamar a una función de plantilla con un tipo de argumento no compatible con los implementados hasta ahora. Evitar esta necesidad es la intención de las plantillas ...
Si bien una pregunta anterior que ha sido respondida por muchos, creo que un método sucinto, no muy diferente de los demás publicados, es utilizar una macro menor para ayudar a facilitar la duplicación de declaraciones de clase.
// abstract.h
// Simply define the types that each concrete class will use
#define IMPL_RENDER() \
void render(int a, char *b) override { render_internal<char>(a, b); } \
void render(int a, short *b) override { render_internal<short>(a, b); } \
// ...
class Renderable
{
public:
// Then, once for each on the abstract
virtual void render(int a, char *a) = 0;
virtual void render(int a, short *b) = 0;
// ...
};
Entonces, para implementar nuestra subclase:
class Box : public Renderable
{
public:
IMPL_RENDER() // Builds the functions we want
private:
template<typename T>
void render_internal(int a, T *b); // One spot for our logic
};
El beneficio aquí es que, al agregar un tipo recientemente admitido, todo se puede hacer desde el encabezado abstracto y renunciar posiblemente a la rectificación en múltiples archivos de origen / encabezado.
Al menos con gcc 5.4, las funciones virtuales podrían ser miembros de plantilla, pero tienen que ser plantillas en sí 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
Prueba esto:
Escribe en classeder.h:
template <typename T>
class Example{
public:
T c_value;
Example(){}
T Set(T variable)
{
return variable;
}
virtual Example VirtualFunc(Example paraM)
{
return paraM.Set(c_value);
}
Marque, si trabaja con esto, para escribir este código en main.cpp:
#include <iostream>
#include <classeder.h>
int main()
{
Example exmpl;
exmpl.c_value = "Hello, world!";
std::cout << exmpl.VirtualFunc(exmpl);
return 0;
}