Słyszałem, że szablony funkcji członka klasy C ++ nie mogą być wirtualne. Czy to prawda?
Jeśli mogą być wirtualne, jaki jest przykład scenariusza, w którym można by użyć takiej funkcji?
Słyszałem, że szablony funkcji członka klasy C ++ nie mogą być wirtualne. Czy to prawda?
Jeśli mogą być wirtualne, jaki jest przykład scenariusza, w którym można by użyć takiej funkcji?
Odpowiedzi:
Szablony dotyczą generowania kodu kompilatora w czasie kompilacji . Funkcje wirtualne polegają na tym, aby system wykonawczy zastanawiał się, którą funkcję wywołać w czasie wykonywania .
Gdy system wykonawczy zorientuje się, że będzie musiał wywołać funkcję wirtualną opartą na szablonie, kompilacja jest zakończona, a kompilator nie może już wygenerować odpowiedniej instancji. Dlatego nie możesz mieć szablonów funkcji wirtualnego członka.
Istnieje jednak kilka potężnych i interesujących technik wynikających z łączenia polimorfizmu i szablonów, w szczególności tak zwane wymazywanie typu .
Virtual functions are all about the run-time system figuring out which function to call at run-time
- przepraszam, ale jest to raczej zły sposób i dość mylący. Jest to tylko pośrednia i nie ma w tym przypadku żadnego „środowiska uruchomieniowego”, w czasie kompilacji wiadomo, że wywoływana funkcja jest wskazywana przez n-ty wskaźnik w tabeli. „Zrozumienie” oznacza, że istnieją kontrole typu i tak nie jest. Once the run-time system figured out it would need to call a templatized virtual function
- w czasie kompilacji wiadomo, czy funkcja jest wirtualna.
void f(concr_base& cb, virt_base& vb) { cb.f(); vb.f(); }
, to „wie”, która funkcja jest wywoływana w punkcie cb.f()
wywoływanym, i nie wie o tym vb.f()
. Ta ostatnia musi znaleźć się w czasie pracy , w systemie czasu . To, czy chcesz nazwać to „rozgryzaniem” i czy jest to mniej lub bardziej wydajne, nie zmienia tych faktów.
Z szablonów C ++ Kompletny przewodnik:
Szablony funkcji składowych nie mogą być deklarowane jako wirtualne. To ograniczenie jest narzucone, ponieważ zwykła implementacja mechanizmu wywoływania funkcji wirtualnej używa tabeli o stałym rozmiarze z jednym wpisem na funkcję wirtualną. Jednak liczba wystąpień szablonu funkcji składowej nie jest ustalona, dopóki cały program nie zostanie przetłumaczony. Dlatego też obsługa szablonów funkcji wirtualnych elementów składowych wymagałaby obsługi zupełnie nowego rodzaju mechanizmu w kompilatorach i linkerach C ++. Natomiast zwykli członkowie szablonów klas mogą być wirtualni, ponieważ ich liczba jest ustalana podczas tworzenia instancji klasy
C ++ nie zezwala obecnie na funkcje wirtualnych elementów szablonu. Najbardziej prawdopodobną przyczyną jest złożoność jego wdrożenia. Rajendra podaje dobry powód, dla którego nie można tego zrobić teraz, ale byłoby to możliwe przy rozsądnych zmianach standardu. Zwłaszcza ustalenie, ile instancji funkcji szablonowej faktycznie istnieje, a zbudowanie tabeli vt wydaje się trudne, jeśli weźmie się pod uwagę miejsce wirtualnego wywołania funkcji. Standardy ludzie mają teraz wiele innych rzeczy do zrobienia, a C ++ 1x to także dużo pracy dla autorów kompilatorów.
Kiedy potrzebujesz szablonowej funkcji członka? Kiedyś natknąłem się na taką sytuację, w której próbowałem refaktoryzować hierarchię za pomocą czystej wirtualnej klasy bazowej. Był to zły styl wdrażania różnych strategii. Chciałem zmienić argument jednej z funkcji wirtualnych na typ liczbowy i zamiast przeciążać funkcję członka i zastępować każde przeciążenie we wszystkich podklasach, próbowałem użyć funkcji wirtualnych szablonów (i musiałem się dowiedzieć, że one nie istnieją .)
Zacznijmy od pewnego tła na temat tabel funkcji wirtualnych i ich działania ( źródło ):
[20.3] Jaka jest różnica między wywoływaniem wirtualnych i niewirtualnych funkcji składowych?
Nie-wirtualne funkcje składowe są rozwiązywane statycznie. Oznacza to, że funkcja członka jest wybierana statycznie (w czasie kompilacji) na podstawie typu wskaźnika (lub odwołania) do obiektu.
Natomiast funkcje wirtualnych elementów są rozwiązywane dynamicznie (w czasie wykonywania). Oznacza to, że funkcja członka jest wybierana dynamicznie (w czasie wykonywania) na podstawie typu obiektu, a nie typu wskaźnika / odwołania do tego obiektu. Nazywa się to „wiązaniem dynamicznym”. Większość kompilatorów stosuje pewien wariant następującej techniki: jeśli obiekt ma jedną lub więcej funkcji wirtualnych, kompilator umieszcza w obiekcie ukryty wskaźnik zwany „wskaźnikiem wirtualnym” lub „wskaźnikiem v”. Ten wskaźnik v wskazuje na globalną tabelę zwaną „wirtualną tabelą” lub „tabelą v”.
Kompilator tworzy tabelę v dla każdej klasy, która ma co najmniej jedną funkcję wirtualną. Na przykład, jeśli klasa Circle ma funkcje wirtualne dla draw () oraz move () i resize (), to z klasą Circle byłby dokładnie jeden stół v, nawet gdyby istniały obiekty gazillionowe Circle, a wskaźnik v każdy z tych obiektów Circle wskazywałby na v-stół Circle. Sama tabela v ma wskaźniki do każdej funkcji wirtualnej w klasie. Na przykład tabela v Circle miałaby trzy wskaźniki: wskaźnik do Circle :: draw (), wskaźnik do Circle :: move () i wskaźnik do Circle :: resize ().
Podczas wysyłania funkcji wirtualnej system wykonawczy podąża za wskaźnikiem v obiektu do tabeli v klasy, a następnie pod odpowiednim gniazdem w tabeli v do kodu metody.
Koszty ogólne związane z powyższą techniką są nominalne: dodatkowy wskaźnik na obiekt (ale tylko w przypadku obiektów, które będą musiały wykonać dynamiczne wiązanie) oraz dodatkowy wskaźnik na metodę (ale tylko w przypadku metod wirtualnych). Narzut kosztów czasu jest również dość nominalny: w porównaniu do normalnego wywołania funkcji, wirtualne wywołanie funkcji wymaga dwóch dodatkowych pobrań (jeden, aby uzyskać wartość wskaźnika v, drugi, aby uzyskać adres metody). Żadne z tych działań środowiska wykonawczego nie występuje w przypadku funkcji niebędących wirtualnymi, ponieważ kompilator rozwiązuje funkcje niebędące wirtualnymi wyłącznie w czasie kompilacji na podstawie typu wskaźnika.
Próbuję teraz użyć czegoś takiego dla klasy bazowej pliku kostek z zoptymalizowanymi funkcjami ładowania zoptymalizowanymi w szablonie, które będą wdrażane inaczej dla różnych typów kostek (niektóre przechowywane w pikselach, inne w obrazach itp.).
Jakiś kod:
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;
To, co chciałbym, żeby było, ale nie będzie się kompilowało z powodu wirtualnego zestawu szablonów:
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;
Skończyłem przenoszenie deklaracji szablonu na poziom klasy . To rozwiązanie zmusiłoby programy do znajomości określonych rodzajów danych, które czytałyby przed ich odczytaniem, co jest niedopuszczalne.
Uwaga, to nie jest bardzo ładne, ale pozwoliło mi to usunąć powtarzający się kod wykonawczy
1) w klasie bazowej
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) oraz w klasach dziecięcych
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);
Zauważ, że LoadAnyCube nie jest zadeklarowany w klasie podstawowej.
Oto kolejna odpowiedź na przepełnienie stosu z obejściem: potrzebujesz obejścia elementu wirtualnego szablonu .
Poniższy kod można skompilować i działa poprawnie, używając MinGW G ++ 3.4.5 w 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);
}
a wynikiem jest:
A:A<string> a
A<--B:B<string> c
A<--B:3
A później dodałem nową klasę X:
class X
{
public:
template <typename T>
virtual void func2(const T& p)
{
cout<<"C:"<<p<<endl;
}
};
Kiedy próbowałem użyć klasy X w main () w następujący sposób:
X x;
x.func2<string>("X x");
g ++ zgłoś następujący błąd:
vtempl.cpp:34: error: invalid use of `virtual' in template declaration of `virtu
al void X::func2(const T&)'
Jest więc oczywiste, że:
Nie, nie mogą. Ale:
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);
};
ma taki sam efekt, jeśli wszystko, co chcesz zrobić, to mieć wspólny interfejs i odroczyć implementację do podklas.
Foo
wskaźnik jest kwalifikowany jako Foo<Bar>
, nie może wskazywać na Foo<Barf>
lub Foo<XXX>
.
Nie, funkcje elementów szablonu nie mogą być wirtualne.
W pozostałych odpowiedziach proponowana funkcja szablonu jest fasadą i nie oferuje żadnych praktycznych korzyści.
Język nie pozwala na korzystanie z funkcji wirtualnych szablonów, ale dzięki obejściu można mieć oba, np. Jedną implementację szablonu dla każdej klasy i wspólny wirtualny interfejs.
Konieczne jest jednak zdefiniowanie dla każdej kombinacji typów szablonów funkcji wirtualnego opakowania wirtualnego:
#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;
}
Wynik:
Pole kwadratu to 1, pole okręgu to 3.1415926535897932385
Wypróbuj tutaj
Aby odpowiedzieć na drugą część pytania:
Jeśli mogą być wirtualne, jaki jest przykład scenariusza, w którym można by użyć takiej funkcji?
To nie jest nierozsądna rzecz, którą chcesz zrobić. Na przykład Java (gdzie każda metoda jest wirtualna) nie ma problemów z metodami ogólnymi.
Jednym z przykładów w C ++ potrzeby szablonu funkcji wirtualnej jest funkcja członka, która akceptuje ogólny iterator. Lub funkcja składowa, która akceptuje ogólny obiekt funkcji.
Rozwiązaniem tego problemu jest użycie funkcji wymazywania z funkcją boost :: any_range i boost :: function, która pozwoli ci zaakceptować ogólny iterator lub funktor bez potrzeby zmiany funkcji w szablon.
Istnieje sposób obejścia „wirtualnej metody szablonu”, jeśli zestaw typów dla metody szablonu jest znany z góry.
Aby pokazać pomysł, w poniższym przykładzie użyto tylko dwóch typów ( int
i double
).
Tam metoda „wirtualnego” szablonu ( Base::Method
) wywołuje odpowiednią metodę wirtualną (jedną z nich Base::VMethod
), która z kolei wywołuje implementację metody szablonu ( Impl::TMethod
).
Wystarczy zaimplementować metodę szablonu TMethod
w implementacjach pochodnych ( AImpl
, BImpl
) i użyć 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;
}
Wynik:
0
1
2
3
NB:
Base::Method
jest faktycznie nadwyżką dla prawdziwego kodu (VMethod
może być upubliczniona i używana bezpośrednio). Dodałem go, aby wyglądał jak rzeczywista metoda „wirtualnego” szablonu.
Base
klasę za każdym razem, gdy musisz wywoływać funkcję szablonu z typem argumentu niezgodnym z tymi, które zostały zaimplementowane do tej pory. Unikanie tej konieczności jest intencją szablonów ...
Podczas gdy starsze pytanie, na które wiele osób odpowiedziało, uważam, że zwięzłą metodą, nie różniącą się tak bardzo od innych opublikowanych, jest użycie drobnego makra, aby ułatwić powielanie deklaracji klasowych.
// 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;
// ...
};
Aby teraz wdrożyć naszą podklasę:
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
};
Zaletą tego jest to, że dodając nowy obsługiwany typ, można to wszystko zrobić z abstrakcyjnego nagłówka i zrezygnować z możliwości jego naprawy w wielu plikach źródłowych / nagłówkowych.
Przynajmniej funkcje wirtualne gcc 5.4 mogą być elementami szablonu, ale same szablony muszą być.
#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;
}
Wyjścia
mix before a2
Process finished with exit code 0
Spróbuj tego:
Napisz w 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);
}
Zaznacz, jeśli pracujesz z tym, aby napisać ten kod w main.cpp:
#include <iostream>
#include <classeder.h>
int main()
{
Example exmpl;
exmpl.c_value = "Hello, world!";
std::cout << exmpl.VirtualFunc(exmpl);
return 0;
}