Ich habe gehört, dass Vorlagen für C ++ - Klassenmitgliedsfunktionen nicht virtuell sein können. Ist das wahr?
Wenn sie virtuell sein können, was ist ein Beispiel für ein Szenario, in dem man eine solche Funktion verwenden würde?
Ich habe gehört, dass Vorlagen für C ++ - Klassenmitgliedsfunktionen nicht virtuell sein können. Ist das wahr?
Wenn sie virtuell sein können, was ist ein Beispiel für ein Szenario, in dem man eine solche Funktion verwenden würde?
Antworten:
In Vorlagen dreht sich alles um den Compiler, der zur Kompilierungszeit Code generiert . Bei virtuellen Funktionen dreht sich alles um das Laufzeitsystem, um herauszufinden, welche Funktion zur Laufzeit aufgerufen werden soll .
Sobald das Laufzeitsystem herausgefunden hat, dass es eine virtuelle Funktion mit Vorlagen aufrufen muss, ist die Kompilierung abgeschlossen und der Compiler kann die entsprechende Instanz nicht mehr generieren. Daher können Sie keine Funktionsvorlagen für virtuelle Elemente haben.
Es gibt jedoch einige leistungsstarke und interessante Techniken, die sich aus der Kombination von Polymorphismus und Vorlagen ergeben, insbesondere die sogenannte Typlöschung .
Virtual functions are all about the run-time system figuring out which function to call at run-time
- Entschuldigung, aber das ist ein ziemlich falscher Weg und ziemlich verwirrend. Es ist nur eine Indirektion, und es ist keine "Laufzeitermittlung" beteiligt. Während der Kompilierungszeit ist bekannt, dass die aufzurufende Funktion diejenige ist, auf die der n-te Zeiger in der vtable zeigt. "Herausfinden" impliziert, dass es Typprüfungen und dergleichen gibt, was nicht der Fall ist. Once the run-time system figured out it would need to call a templatized virtual function
- Ob die Funktion virtuell ist oder nicht, ist zur Kompilierungszeit bekannt.
void f(concr_base& cb, virt_base& vb) { cb.f(); vb.f(); }
, "weiß" er, welche Funktion an dem cb.f()
aufgerufenen Punkt aufgerufen wird, und weiß das nicht für vb.f()
. Letztere muss herausgefunden, zur Laufzeit , durch das Laufzeitsystem . Ob Sie dies als "Herausfinden" bezeichnen möchten und ob dies mehr oder weniger effizient ist, ändert nichts an diesen Fakten.
Aus C ++ - Vorlagen Das vollständige Handbuch:
Mitgliedsfunktionsvorlagen können nicht als virtuell deklariert werden. Diese Einschränkung wird auferlegt, da bei der üblichen Implementierung des Aufrufmechanismus für virtuelle Funktionen eine Tabelle mit fester Größe mit einem Eintrag pro virtueller Funktion verwendet wird. Die Anzahl der Instanziierungen einer Elementfunktionsvorlage wird jedoch erst festgelegt, wenn das gesamte Programm übersetzt wurde. Die Unterstützung von Funktionsvorlagen für virtuelle Elemente würde daher die Unterstützung einer völlig neuen Art von Mechanismus in C ++ - Compilern und Linkern erfordern. Im Gegensatz dazu können die normalen Mitglieder von Klassenvorlagen virtuell sein, da ihre Anzahl festgelegt ist, wenn eine Klasse instanziiert wird
C ++ erlaubt derzeit keine Funktionen für virtuelle Vorlagenmitglieder. Der wahrscheinlichste Grund ist die Komplexität der Implementierung. Rajendra gibt einen guten Grund an, warum dies derzeit nicht möglich ist, aber es könnte mit vernünftigen Änderungen des Standards möglich sein. Insbesondere wenn Sie den Ort des virtuellen Funktionsaufrufs berücksichtigen, ist es schwierig, herauszufinden, wie viele Instanziierungen einer Vorlagenfunktion tatsächlich vorhanden sind, und die vtable aufzubauen. Standard-Leute haben gerade eine Menge anderer Dinge zu tun und C ++ 1x ist auch für die Compiler-Autoren eine Menge Arbeit.
Wann würden Sie eine Vorlagenelementfunktion benötigen? Ich bin einmal auf eine solche Situation gestoßen, in der ich versucht habe, eine Hierarchie mit einer reinen virtuellen Basisklasse umzugestalten. Es war ein schlechter Stil für die Umsetzung verschiedener Strategien. Ich wollte das Argument einer der virtuellen Funktionen in einen numerischen Typ ändern und anstatt die Elementfunktion zu überladen und jede Überladung in allen Unterklassen zu überschreiben, habe ich versucht, virtuelle Vorlagenfunktionen zu verwenden (und musste herausfinden, dass sie nicht existieren .)
Beginnen wir mit einigen Hintergrundinformationen zu virtuellen Funktionstabellen und ihrer Funktionsweise ( Quelle ):
[20.3] Was ist der Unterschied zwischen dem Aufruf virtueller und nicht virtueller Mitgliedsfunktionen?
Nicht virtuelle Mitgliedsfunktionen werden statisch aufgelöst. Das heißt, die Elementfunktion wird statisch (zur Kompilierungszeit) basierend auf dem Typ des Zeigers (oder der Referenz) auf das Objekt ausgewählt.
Im Gegensatz dazu werden virtuelle Elementfunktionen dynamisch (zur Laufzeit) aufgelöst. Das heißt, die Elementfunktion wird dynamisch (zur Laufzeit) basierend auf dem Typ des Objekts ausgewählt, nicht auf dem Typ des Zeigers / Verweises auf dieses Objekt. Dies wird als "dynamische Bindung" bezeichnet. Die meisten Compiler verwenden eine Variante der folgenden Technik: Wenn das Objekt eine oder mehrere virtuelle Funktionen hat, fügt der Compiler einen versteckten Zeiger in das Objekt ein, der als "virtueller Zeiger" oder "v-Zeiger" bezeichnet wird. Dieser V-Zeiger zeigt auf eine globale Tabelle, die als "virtuelle Tabelle" oder "V-Tabelle" bezeichnet wird.
Der Compiler erstellt eine V-Tabelle für jede Klasse mit mindestens einer virtuellen Funktion. Wenn die Klasse Circle beispielsweise virtuelle Funktionen für draw () und move () und resize () hat, ist der Klasse Circle genau eine V-Tabelle zugeordnet, selbst wenn es eine Unmenge von Circle-Objekten und den V-Zeiger von gibt Jedes dieser Circle-Objekte würde auf die Circle-V-Tabelle verweisen. Die V-Tabelle selbst enthält Zeiger auf jede der virtuellen Funktionen in der Klasse. Beispielsweise hätte die Circle-V-Tabelle drei Zeiger: einen Zeiger auf Circle :: draw (), einen Zeiger auf Circle :: move () und einen Zeiger auf Circle :: resize ().
Während eines Versands einer virtuellen Funktion folgt das Laufzeitsystem dem V-Zeiger des Objekts auf die V-Tabelle der Klasse und folgt dann dem entsprechenden Slot in der V-Tabelle zum Methodencode.
Der Platzaufwand für die oben genannte Technik ist nominal: ein zusätzlicher Zeiger pro Objekt (jedoch nur für Objekte, für die eine dynamische Bindung erforderlich ist) sowie ein zusätzlicher Zeiger pro Methode (jedoch nur für virtuelle Methoden). Der Zeitaufwand ist ebenfalls relativ gering: Im Vergleich zu einem normalen Funktionsaufruf erfordert ein virtueller Funktionsaufruf zwei zusätzliche Abrufe (einen zum Abrufen des Werts des V-Zeigers und einen zweiten zum Abrufen der Adresse der Methode). Keine dieser Laufzeitaktivitäten findet bei nicht virtuellen Funktionen statt, da der Compiler nicht virtuelle Funktionen ausschließlich zur Kompilierungszeit basierend auf dem Typ des Zeigers auflöst.
Ich versuche jetzt, so etwas für eine Cubefile-Basisklasse mit vorlagenoptimierten Ladefunktionen zu verwenden, die für verschiedene Arten von Cubes unterschiedlich implementiert werden (einige nach Pixel, andere nach Bild usw.).
Etwas Code:
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;
Was ich gerne hätte, aber es wird aufgrund einer Kombination aus virtuellen Vorlagen nicht kompiliert:
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;
Am Ende habe ich die Vorlagendeklaration auf die Klassenebene verschoben . Diese Lösung hätte Programme gezwungen, bestimmte Datentypen zu kennen, die sie lesen würden, bevor sie sie lesen, was nicht akzeptabel ist.
Warnung, das ist nicht sehr hübsch, aber es hat mir erlaubt, sich wiederholenden Ausführungscode zu entfernen
1) in der Basisklasse
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) und in den Kinderklassen
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);
Beachten Sie, dass LoadAnyCube in der Basisklasse nicht deklariert ist.
Hier ist eine weitere Antwort zum Stapelüberlauf mit einer Problemumgehung: Sie benötigen eine Problemumgehung für virtuelle Vorlagenmitglieder .
Der folgende Code kann mit MinGW G ++ 3.4.5 unter Windows 7 kompiliert und ordnungsgemäß ausgeführt werden:
#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);
}
und die Ausgabe ist:
A:A<string> a
A<--B:B<string> c
A<--B:3
Und später habe ich eine neue Klasse X hinzugefügt:
class X
{
public:
template <typename T>
virtual void func2(const T& p)
{
cout<<"C:"<<p<<endl;
}
};
Als ich versuchte, Klasse X in main () wie folgt zu verwenden:
X x;
x.func2<string>("X x");
g ++ meldet den folgenden Fehler:
vtempl.cpp:34: error: invalid use of `virtual' in template declaration of `virtu
al void X::func2(const T&)'
Es ist also offensichtlich, dass:
Nein, können sie nicht. Aber:
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);
};
hat fast den gleichen Effekt, wenn Sie nur eine gemeinsame Schnittstelle haben und die Implementierung auf Unterklassen verschieben möchten.
Foo
Zeiger ist qualifiziert als Foo<Bar>
, er kann nicht auf ein Foo<Barf>
oder zeigen Foo<XXX>
.
Nein, Vorlagenelementfunktionen können nicht virtuell sein.
In den anderen Antworten ist die vorgeschlagene Vorlagenfunktion eine Fassade und bietet keinen praktischen Nutzen.
Die Sprache erlaubt keine virtuellen Vorlagenfunktionen, aber mit einer Problemumgehung ist es möglich, beide zu haben, z. B. eine Vorlagenimplementierung für jede Klasse und eine virtuelle gemeinsame Schnittstelle.
Es ist jedoch erforderlich, für jede Kombination von Vorlagentypen eine virtuelle Dummy-Wrapper-Funktion zu definieren:
#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;
}
Ausgabe:
Die quadratische Fläche beträgt 1, die Kreisfläche beträgt 3,1415926535897932385
Probieren Sie es hier aus
Um den zweiten Teil der Frage zu beantworten:
Wenn sie virtuell sein können, was ist ein Beispiel für ein Szenario, in dem man eine solche Funktion verwenden würde?
Dies ist keine unvernünftige Sache. Zum Beispiel hat Java (wo jede Methode virtuell ist) keine Probleme mit generischen Methoden.
Ein Beispiel in C ++ für den Wunsch nach einer virtuellen Funktionsvorlage ist eine Elementfunktion, die einen generischen Iterator akzeptiert. Oder eine Mitgliedsfunktion, die ein generisches Funktionsobjekt akzeptiert.
Die Lösung für dieses Problem besteht darin, die Typlöschung mit der Funktion boost :: any_range und boost :: zu verwenden, mit der Sie einen generischen Iterator oder Funktor akzeptieren können, ohne Ihre Funktion zu einer Vorlage machen zu müssen.
Es gibt eine Problemumgehung für die 'virtuelle Vorlagenmethode', wenn eine Reihe von Typen für die Vorlagenmethode im Voraus bekannt ist.
Um die Idee zu zeigen, werden im folgenden Beispiel nur zwei Typen verwendet ( int
und double
).
Dort Base::Method
ruft eine 'virtuelle' Vorlagenmethode ( ) die entsprechende virtuelle Methode (eine von Base::VMethod
) auf, die wiederum die Implementierung der Vorlagenmethode ( ) aufruft Impl::TMethod
.
Man muss nur die Template-Methode TMethod
in abgeleiteten Implementierungen ( AImpl
, BImpl
) implementieren und verwenden 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;
}
Ausgabe:
0
1
2
3
NB:
Base::Method
ist tatsächlich ein Überschuss für echten Code ( VMethod
kann veröffentlicht und direkt verwendet werden). Ich habe es hinzugefügt, damit es wie eine tatsächliche "virtuelle" Vorlagenmethode aussieht.
Base
Klasse jedes Mal ändern müssen, wenn Sie eine Vorlagenfunktion mit einem Argumenttyp aufrufen müssen, der nicht mit den bisher implementierten kompatibel ist. Diese Notwendigkeit zu vermeiden ist die Absicht von Vorlagen ...
Während eine ältere Frage, die von vielen beantwortet wurde, meiner Meinung nach eine prägnante Methode ist, die sich nicht so stark von den anderen unterscheidet, ist die Verwendung eines kleinen Makros, um das Duplizieren von Klassendeklarationen zu erleichtern.
// 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;
// ...
};
Um nun unsere Unterklasse zu implementieren:
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
};
Der Vorteil hierbei ist, dass beim Hinzufügen eines neu unterstützten Typs alles über den abstrakten Header erfolgen kann und möglicherweise nicht in mehreren Quell- / Header-Dateien korrigiert werden kann.
Zumindest mit gcc 5.4 könnten virtuelle Funktionen Vorlagenmitglieder sein, müssen aber selbst Vorlagen sein.
#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;
}
Ausgänge
mix before a2
Process finished with exit code 0
Versuche dies:
Schreiben Sie in 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);
}
Überprüfen Sie, ob Sie damit arbeiten, um diesen Code in main.cpp zu schreiben:
#include <iostream>
#include <classeder.h>
int main()
{
Example exmpl;
exmpl.c_value = "Hello, world!";
std::cout << exmpl.VirtualFunc(exmpl);
return 0;
}