Otrzymałem to pytanie, gdy otrzymałem komentarz do recenzji kodu, który mówi, że funkcje wirtualne nie muszą być wbudowane.
Pomyślałem, że wbudowane funkcje wirtualne mogą się przydać w scenariuszach, w których funkcje są wywoływane bezpośrednio na obiektach. Ale przyszedł mi do głowy kontrargument - dlaczego ktoś miałby chcieć definiować wirtualne, a następnie używać obiektów do wywoływania metod?
Czy najlepiej nie używać wbudowanych funkcji wirtualnych, ponieważ i tak prawie nigdy nie są one rozbudowywane?
Fragment kodu użyty do analizy:
class Temp
{
public:
virtual ~Temp()
{
}
virtual void myVirtualFunction() const
{
cout<<"Temp::myVirtualFunction"<<endl;
}
};
class TempDerived : public Temp
{
public:
void myVirtualFunction() const
{
cout<<"TempDerived::myVirtualFunction"<<endl;
}
};
int main(void)
{
TempDerived aDerivedObj;
//Compiler thinks it's safe to expand the virtual functions
aDerivedObj.myVirtualFunction();
//type of object Temp points to is always known;
//does compiler still expand virtual functions?
//I doubt compiler would be this much intelligent!
Temp* pTemp = &aDerivedObj;
pTemp->myVirtualFunction();
return 0;
}
pTemp->myVirtualFunction()
może to zostać rozwiązane jako wywołanie niewirtualne, mogłoby mieć to wywołanie wbudowane. To wywołanie, do którego się odwołujemy, jest wstawione przez g ++ 3.4.2:TempDerived & pTemp = aDerivedObj; pTemp.myVirtualFunction();
Twój kod nie jest.Odpowiedzi:
Funkcje wirtualne można czasami wstawić. Fragment znakomitego FAQ C ++ :
źródło
C ++ 11 został dodany
final
. To zmienia akceptowaną odpowiedź: nie trzeba już znać dokładnej klasy obiektu, wystarczy wiedzieć, że obiekt ma co najmniej typ klasy, w której funkcja została zadeklarowana jako ostateczna:źródło
icc
wydaje się, że to robi, zgodnie z tym linkiem.Jest jedna kategoria funkcji wirtualnych, w których nadal warto mieć je wbudowane. Rozważ następujący przypadek:
Wywołanie usunięcia „bazy” wykona wirtualne wywołanie w celu wywołania prawidłowego destruktora klasy pochodnej, wywołanie to nie jest wbudowane. Jednak ponieważ każdy destruktor wywołuje swój destruktor nadrzędny (który w tych przypadkach jest pusty), kompilator może wstawiać te wywołania, ponieważ nie wywołują one wirtualnie funkcji klasy bazowej.
Ta sama zasada istnieje dla konstruktorów klas bazowych lub dla dowolnego zestawu funkcji, w których implementacja pochodna wywołuje również implementację klas podstawowych.
źródło
Widziałem kompilatory, które nie emitują żadnej tabeli v-table, jeśli w ogóle nie istnieje funkcja nie-inline (i zdefiniowana w jednym pliku implementacji zamiast nagłówka). Rzucaliby błędy takie jak
missing vtable-for-class-A
lub coś podobnego, a ty byłbyś zdezorientowany jak diabli, tak jak ja.Rzeczywiście, nie jest to zgodne ze standardem, ale zdarza się, więc rozważ umieszczenie co najmniej jednej funkcji wirtualnej poza nagłówkiem (choćby wirtualnym destruktorem), aby kompilator mógł w tym miejscu wyemitować tabelę vtable dla klasy. Wiem, że to się dzieje z niektórymi wersjami
gcc
.Jak ktoś wspomniał, funkcje wirtualne inline mogą czasami przynieść korzyści , ale oczywiście najczęściej będziesz ich używać, gdy nie znasz dynamicznego typu obiektu, bo to był
virtual
przede wszystkim powód .Jednak kompilator nie może całkowicie zignorować
inline
. Oprócz przyspieszania wywołania funkcji ma inną semantykę. Niejawna inline do definicji w klasie jest mechanizm, który pozwala umieścić definicję w nagłówku: Tylkoinline
funkcje można zdefiniować kilka razy w ciągu całego programu bez naruszenia jakichkolwiek przepisów. Ostatecznie zachowuje się tak, jakbyś to zdefiniował tylko raz w całym programie, mimo że wielokrotnie dołączałeś nagłówek do różnych połączonych ze sobą plików.źródło
Cóż, w rzeczywistości funkcje wirtualne zawsze mogą być wstawiane , o ile są ze sobą połączone statycznie: załóżmy, że mamy klasę abstrakcyjną
Base
z funkcją wirtualnąF
i klasami pochodnymiDerived1
orazDerived2
:Hipotetyczne wezwanie
b->F();
(zb
typemBase*
) jest oczywiście wirtualne. Ale ty (lub kompilator ...) możesz przepisać to w ten sposób (przypuśćmy, żetypeof
jest totypeid
funkcja podobna do-, która zwraca wartość, której można użyć w aswitch
)podczas gdy nadal potrzebujemy RTTI dla tego
typeof
, wywołanie można skutecznie wstawić poprzez, w zasadzie, osadzenie tabeli vtable w strumieniu instrukcji i wyspecjalizowanie wywołania dla wszystkich zaangażowanych klas. Można to również uogólnić, specjalizując się tylko w kilku klasach (powiedzmy po prostuDerived1
):źródło
Zaznaczenie metody wirtualnej w tekście pomaga w dalszej optymalizacji funkcji wirtualnych w dwóch następujących przypadkach:
Ciekawie powtarzający się wzór szablonu ( http://www.codeproject.com/Tips/537606/Cplusplus-Prefer-Curiously-Recurring-Template-Patt )
Zastąpienie metod wirtualnych szablonami ( http://www.di.unipi.it/~nids/docs/templates_vs_inheritance.html )
źródło
inline naprawdę nic nie robi - to podpowiedź. Kompilator może to zignorować lub może wbudować zdarzenie wywołania bez inline, jeśli zobaczy implementację i polubi ten pomysł. Jeśli w grę wchodzi przejrzystość kodu, należy usunąć inline .
źródło
Deklarowane wbudowane funkcje wirtualne są wstawiane, gdy są wywoływane przez obiekty i ignorowane, gdy są wywoływane za pomocą wskaźnika lub referencji.
źródło
Dzięki nowoczesnym kompilatorom nie zaszkodzi ich włączenie. Niektóre starożytne kombinacje kompilator / linker mogły tworzyć wiele tabel vtables, ale nie sądzę, aby to już był problem.
źródło
Kompilator może wbudować funkcję tylko wtedy, gdy wywołanie można jednoznacznie rozpoznać w czasie kompilacji.
Funkcje wirtualne są jednak rozwiązywane w czasie wykonywania, więc kompilator nie może wbudować wywołania, ponieważ w typie kompilacji nie można określić typu dynamicznego (a zatem implementacji funkcji do wywołania).
źródło
W przypadkach, gdy wywołanie funkcji jest jednoznaczne, a funkcja jest odpowiednim kandydatem do wstawienia, kompilator jest wystarczająco inteligentny, aby i tak wstawić kod.
Reszta czasu "inline virtual" to nonsens i rzeczywiście, niektóre kompilatory nie skompilują tego kodu.
źródło
Tworzenie funkcji wirtualnych, a następnie wywoływanie ich na obiektach zamiast odniesień lub wskaźników ma sens. Scott Meyer zaleca w swojej książce „efektywne c ++”, aby nigdy nie redefiniować odziedziczonej funkcji niewirtualnej. Ma to sens, ponieważ kiedy tworzysz klasę z niewirtualną funkcją i redefiniujesz funkcję w klasie pochodnej, możesz być pewien, że sam użyjesz jej poprawnie, ale nie możesz być pewien, że inni będą jej używać poprawnie. Możesz także później samodzielnie go niepoprawnie używać. Tak więc, jeśli utworzysz funkcję w klasie bazowej i chcesz, aby była możliwa do ponownego zdefiniowania, powinieneś uczynić ją wirtualną. Jeśli ma sens tworzenie funkcji wirtualnych i wywoływanie ich na obiektach, sensowne jest również ich wstawianie.
źródło
Właściwie w niektórych przypadkach dodanie „inline” do wirtualnego nadpisania końcowego może spowodować, że kod nie zostanie skompilowany, więc czasami występuje różnica (przynajmniej pod kompilatorem VS2017)!
Właściwie robiłem wirtualną funkcję ostatecznego nadpisywania w linii w VS2017, dodając standard c ++ 17 do kompilacji i linkowania iz jakiegoś powodu zawiodło, gdy używam dwóch projektów.
Miałem projekt testowy i bibliotekę DLL implementacji, którą testuję. W projekcie testowym mam plik „linker_includes.cpp”, który # zawiera pliki * .cpp z innego projektu, które są potrzebne. Wiem ... Wiem, że mogę ustawić MSBuild tak, aby używał plików obiektowych z biblioteki DLL, ale pamiętaj, że jest to rozwiązanie specyficzne dla firmy Microsoft, podczas gdy dołączanie plików cpp nie jest powiązane z systemem kompilacji i znacznie łatwiejsze do wersji plik cpp niż pliki xml i ustawienia projektu i takie ...
Co ciekawe, cały czas otrzymywałem błąd linkera z projektu testowego. Nawet jeśli dodałem definicję brakujących funkcji przez kopiowanie wklejania, a nie przez dołączanie! Bardzo dziwne. Drugi projekt został zbudowany i nie ma między nimi żadnego połączenia poza oznaczeniem odniesienia do projektu, więc istnieje kolejność kompilacji, która zapewnia, że oba są zawsze zbudowane ...
Myślę, że to jakiś błąd w kompilatorze. Nie mam pojęcia, czy istnieje w kompilatorze dostarczonym z VS2020, ponieważ używam starszej wersji, ponieważ niektóre SDK działają tylko z tym poprawnie :-(
Chciałem tylko dodać, że nie tylko oznaczanie ich jako wbudowanych może coś znaczyć, ale może nawet sprawić, że twój kod nie zostanie zbudowany w rzadkich przypadkach! To dziwne, ale dobrze wiedzieć.
PS .: Kod, nad którym pracuję, jest związany z grafiką komputerową, więc wolę inlining i dlatego użyłem zarówno final, jak i inline. Zachowałem ostateczny specyfikator, aby mieć nadzieję, że kompilacja wydania jest wystarczająco inteligentna, aby zbudować bibliotekę DLL, wstawiając ją nawet bez bezpośredniej aluzji, więc ...
PS (Linux) .: Spodziewam się, że to samo nie dzieje się w gcc lub clang, jak zwykle robiłem tego typu rzeczy. Nie jestem pewien, skąd bierze się ten problem ... Wolę robić c ++ na Linuksie lub przynajmniej z jakimś gcc, ale czasami projekt ma inne potrzeby.
źródło