2009-04-09 11 views
145

Mam to pytanie, gdy otrzymałem komentarz z komentarzem do kodu, mówiąc, że funkcje wirtualne nie muszą być wstawiane.Czy wbudowane funkcje wirtualne są naprawdę pozbawione sensu?

Pomyślałem, że wbudowane funkcje wirtualne mogą się przydać w scenariuszach, w których funkcje są wywoływane bezpośrednio na obiektach. Ale moim przeciw-argumentem stało się - dlaczego chciałoby się zdefiniować wirtualne, a następnie użyć obiektów do wywoływania metod?

Najlepiej nie używać wbudowanych funkcji wirtualnych, ponieważ i tak prawie nigdy nie są one rozwijane?

Fragment kodu użyłem 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; 
} 
+0

Rozważmy kompilacji przykład z niezależnie od potrzebnych przełączników, aby uzyskać listę asemblerów, a następnie pokazując przeglądarkę kodu, która rzeczywiście może kompilować wbudowane funkcje wirtualne. –

+0

Powyższe z reguły nie będzie inline, ponieważ wywołujesz funkcję wirtualną na poziomie klasy bazowej. Chociaż zależy to tylko od tego, jak inteligentny jest kompilator. Gdyby można było wskazać, że 'pTemp-> myVirtualFunction()' może być rozpoznany jako połączenie inne niż wirtualne, może zainicjować to połączenie. To wywołanie referencyjne jest wstawiane za pomocą g ++ 3.4.2: 'TempDerived & pTemp = aDerivedObj; pTemp.myVirtualFunction(); 'Twój kod nie jest. – doc

+0

Jedną z rzeczy, którą faktycznie robi gcc, jest porównanie wejścia vtable z konkretnym symbolem, a następnie użycie wstawionego wariantu w pętli, jeśli pasuje. Jest to szczególnie użyteczne, jeśli funkcja inlined jest pusta, a pętlę można w tym przypadku wyeliminować. –

Odpowiedz

125

Funkcje wirtualne można czasami wstawiać. Wyciąg z doskonałą C++ faq:

„Jedyny czas inline wirtualnego call można inlined jest, gdy kompilator wie«dokładnie klasy»obiektu który jest celem wirtualnego połączenia funkcji . może się to zdarzyć tylko gdy kompilator ma rzeczywistą obiektu zamiast wskaźnik lub odniesienie do przedmiot, tj. zarówno z lokalnego obiektu, obiekt globalny/statyczne albo całkowicie zamkniętego obiektu wewnątrz kompozytu . "

+6

To prawda, ale warto pamiętać, że kompilator może zignorować wbudowany specyfikator, nawet jeśli wywołanie można rozwiązać w czasie kompilacji i można je wstawić. – sharptooth

+6

Inną sytuacją, gdy myślę, że inline może się zdarzyć, jest to, że gdy wywołasz metodę na przykład jako> Temp :: myVirtualFunction() - takie wywołanie pomija rozdzielczość wirtualnego stołu, a funkcja powinna być ustawiona bezproblemowo - dlaczego i jeśli "Chcę to zrobić to inny temat :) – RnR

+5

@RnR. Nie trzeba mieć "this->", wystarczy użyć nazwy kwalifikowanej. I to zachowanie ma miejsce dla destruktorów, konstruktorów i ogólnie dla operatorów przypisania (zobacz moją odpowiedź). –

0

Kompilator może tylko funkcją inline, gdy połączenie może zostać rozwiązany w sposób jednoznaczny w czasie kompilacji.

Funkcje wirtualne są jednak rozwiązywane w środowisku wykonawczym, więc kompilator nie może wstawić wywołania, ponieważ podczas kompilacji typ dynamiczny (a zatem wywoływana funkcja) nie może zostać określony.

+1

Gdy wywołujesz metodę klasy bazowej z tej samej lub pochodnej klasy, wywołanie jest jednoznaczne i nie-wirtualne – sharptooth

+1

@sharptooth: ale wtedy byłaby ona metodą inline nie-wirtualną. Kompilator może wstawiać funkcje, o które nie prosisz, i prawdopodobnie wie lepiej, kiedy wstawiać lub nie. Niech to się zdecyduje. –

+1

@dribeas: Tak, właśnie o tym mówię. Sprzeciwiłem się tylko stwierdzeniu, że wirtualne finalizacje są rozwiązywane w czasie wykonywania - jest to prawdą tylko wtedy, gdy wywołanie odbywa się wirtualnie, a nie dla dokładnej klasy. – sharptooth

3

wbudowany naprawdę nic nie robi - to podpowiedź. Kompilator może to zignorować lub może zainicjować zdarzenie wywołania bez wbudowanego, jeśli widzi implementację i lubi ten pomysł. Jeśli wymagana jest klarowność kodu, należy usunąć wbudowany.

+2

Kompilatory, które działają tylko w pojedynczych JT, mogą wstawiać tylko niejawnie funkcje, dla których mają definicję. Funkcję można zdefiniować tylko w wielu JT, jeśli ją wprowadzisz. "inline" jest czymś więcej niż podpowiedź i może mieć dramatyczny wzrost wydajności dla kompilacji g ++/makefile. –

1

Dzięki nowoczesnym kompilatorom nie zaszkodzi im ich brak. Niektóre starsze kompilatory kompilatorów/łączników mogły utworzyć wiele tabel, ale nie sądzę, że to już jest problem.

1

W przypadkach, w których wywołanie funkcji jest jednoznaczne, a funkcja jest odpowiednim kandydatem do wstawiania, kompilator jest wystarczająco inteligentny, aby mimo wszystko wstawić kod.

Reszta czasu "inline virtual" jest nonsensem i rzeczywiście niektóre kompilatory nie skompilują tego kodu.

+0

Która wersja g ++ nie będzie kompilować wirtualnych wirtualnych? –

+0

Hm. 4.1.1 Mam tutaj teraz wydaje się być szczęśliwy. Po raz pierwszy napotkałem problemy z tym kodem przy użyciu wersji 4.0.x. Zgadnij, że moje informacje są nieaktualne, edytowane. – moonshadow

33

Istnieje jedna kategoria funkcji wirtualnych, w których nadal ma sens umieszczanie ich w linii. Rozważmy następujący przypadek:

class Base { 
public: 
    inline virtual ~Base() { } 
}; 

class Derived1 : public Base { 
    inline virtual ~Derived1() { } // Implicitly calls Base::~Base(); 
}; 

class Derived2 : public Derived1 { 
    inline virtual ~Derived2() { } // Implicitly calls Derived1::~Derived1(); 
}; 

void foo (Base * base) { 
    delete base;    // Virtual call 
} 

Wezwanie do usunięcia „bazy”, wystąpi wirtualnego połączenia, aby połączyć poprawną pochodzący klasy destruktor, to połączenie nie jest inlined. Ponieważ jednak każdy destruktor nazywa go destruktem nadrzędnym (który w tych przypadkach jest pusty), kompilator może wywoływać wywołania, ponieważ nie wywołują one praktycznie żadnych funkcji klasy podstawowej.

Ta sama zasada istnieje dla konstruktorów klasy podstawowej lub dla dowolnego zestawu funkcji, w których pochodna implementacja wywołuje również implementację klas bazowych.

+19

Należy jednak pamiętać, że puste nawiasy nie zawsze oznaczają, że destruktor nic nie robi. Destruktory domyślnie - niszczą każdy obiekt członkowski w klasie, więc jeśli masz kilka wektorów w klasie bazowej, to może być całkiem sporo pracy w pustych nawiasach klamrowych! – Philip

+0

@Philip: Dobrze. –

12

Widziałem kompilatory, które nie emitują żadnego v-table, jeśli nie istnieje żadna funkcja inline (i zdefiniowana w jednym pliku implementacji zamiast nagłówka). Rzucałyby błędy, takie jak missing vtable-for-class-A lub coś podobnego, i byłbyś zdezorientowany jak diabli, tak jak ja.

Rzeczywiście, nie jest to zgodne ze Standardem, ale tak się dzieje, więc należy rozważyć umieszczenie co najmniej jednej wirtualnej funkcji nie w nagłówku (jeśli jest to tylko wirtualny destruktor), aby kompilator mógł emitować tabelę vtable dla klasy w tym miejscu . Wiem, że dzieje się tak z niektórymi wersjami gcc.

Jak ktoś wspomniał, wbudowane funkcje wirtualne mogą być korzyści czasami, ale oczywiście najczęściej można go używać podczas wykonywania nie wiedzieć dynamiczny typ obiektu, bo to była cała przyczyna virtual na pierwszym miejscu.

Jednak kompilator nie może całkowicie zignorować inline. Ma inną semantykę oprócz przyspieszenia wywołania funkcji. domyślnie wbudowany dla definicji w klasie jest mechanizm, który pozwala umieścić definicję w nagłówku: Tylko funkcje inline można zdefiniować wiele razy w całym programie bez naruszania jakichkolwiek zasad. Ostatecznie zachowuje się tak, jak byś zdefiniował to tylko raz w całym programie, nawet jeśli wielokrotnie umieściłeś nagłówek w różnych połączonych ze sobą plikach.

3

Deklinowane funkcje wirtualne są wstawiane, gdy są wywoływane przez obiekty i ignorowane, gdy są wywoływane przez wskaźnik lub odwołania.

10

Właściwie funkcje wirtualne mogą zawsze być inlined, jak długo są one ze sobą powiązane statycznie: załóżmy, że mamy klasę abstrakcyjną Base z wirtualnym funkcji F i klas pochodnych Derived1 i Derived2:

class Base { 
    virtual void F() = 0; 
}; 

class Derived1 : public Base { 
    virtual void F(); 
}; 

class Derived2 : public Base { 
    virtual void F(); 
}; 

Hipotetyczna rozmowa b->F(); (z b typu Base*) jest oczywiście wirtualna. Ale ty (lub compiler ...) mógłby przerobić go tak jak (załóżmy typeof jest typeid -jak funkcja, która zwraca wartość, która może być użyta w switch)

switch (typeof(b)) { 
    case Derived1: b->Derived1::F(); break; // static, inlineable call 
    case Derived2: b->Derived2::F(); break; // static, inlineable call 
    case Base:  assert(!"pure virtual function call!"); 
    default:  b->F(); break; // virtual call (dyn-loaded code) 
} 

natomiast wciąż musimy RTTI dla typeof, połączenie może być skutecznie zainicjowane przez, w zasadzie, osadzenie tabeli vtable w strumieniu instrukcji i wyspecjalizowanie wywołania dla wszystkich zaangażowanych klas. Może to być także uogólnione przez specjalizującą się tylko kilka klas (powiedzmy tylko Derived1):

switch (typeof(b)) { 
    case Derived1: b->Derived1::F(); break; // hot path 
    default:  b->F(); break; // default virtual call, cold path 
} 
0

To ma sensu, aby funkcje wirtualne, a następnie połączyć je w obiektach, a nie referencje lub wskaźniki. Scott Meyer zaleca, w swojej książce "Skuteczne C++", aby nigdy nie redefiniować dziedziczonej funkcji nie-wirtualnej. Ma to sens, ponieważ gdy tworzysz klasę z funkcją inną niż wirtualna i na nowo definiujesz funkcję w klasie pochodnej, możesz być pewien, że użyjesz jej poprawnie, ale nie możesz być pewien, że inni będą jej używać poprawnie. Ponadto możesz później użyć go nieprawidłowo.Tak więc, jeśli tworzysz funkcję w klasie bazowej i chcesz, aby była możliwa do zmienienia, powinieneś uczynić ją wirtualną. Jeśli ma sens tworzenie funkcji wirtualnych i wywoływanie ich na obiektach, ma również sens wstawianie ich.

59

C++ 11 dodał final. Zmienia to zaakceptowane odpowiedź: to już nie jest konieczne, aby poznać dokładną klasę obiektu, to wystarczy znać przedmiot ma co najmniej typ klasy, w których funkcja została zadeklarowana końcowa:

class A { 
    virtual void foo(); 
}; 
class B : public A { 
    inline virtual void foo() final { } 
}; 
class C : public B 
{ 
}; 

void bar(B const& b) { 
    A const& a = b; // Allowed, every B is an A. 
    a.foo(); // Call to B::foo() can be inlined, even if b is actually a class C. 
} 
+0

Nie można wstawić tego w VS 2017. – Yola

+0

Nie sądzę, że to działa w ten sposób. Wywołanie foo() przez wskaźnik/odwołanie typu A nigdy nie może zostać wstawione. Wywołanie funkcji b.foo() powinno umożliwiać wstawianie. Chyba, że ​​sugerujesz, że kompilator już wie, że jest to typ B, ponieważ jest świadomy poprzedniej linii. Ale to nie jest typowe użycie. –

+0

Na przykład porównaj wygenerowany kod dla paska i basu: https://godbolt.org/g/xy3rNh –

Powiązane problemy