Czy wbudowane funkcje wirtualne naprawdę nie mają sensu?


172

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;
}

1
Rozważ skompilowanie przykładu z dowolnymi przełącznikami potrzebnymi do uzyskania listy asemblera, a następnie pokazanie recenzentowi kodu, że rzeczywiście kompilator może wbudować funkcje wirtualne.
Thomas L Holaday

1
Powyższe zwykle nie będzie wstawiane, ponieważ wywołujesz funkcję wirtualną w celu pomocy klasy bazowej. Chociaż zależy to tylko od tego, jak inteligentny jest kompilator. Gdyby był w stanie wskazać, że 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.
doc

1
Jedną rzeczą, którą faktycznie robi gcc, jest porównanie wpisu vtable z określonym symbolem, a następnie użycie w pętli wariantu inline, jeśli pasuje. Jest to szczególnie przydatne, jeśli funkcja wstawiana jest pusta i pętla może zostać w tym przypadku wyeliminowana.
Simon Richter,

1
@doc Nowoczesny kompilator bardzo się stara określić w czasie kompilacji możliwe wartości wskaźników. Samo użycie wskaźnika nie wystarczy, aby zapobiec wstawianiu na jakimkolwiek znaczącym poziomie optymalizacji; GCC dokonuje nawet uproszczeń przy zerowej optymalizacji!
curiousguy

Odpowiedzi:


153

Funkcje wirtualne można czasami wstawić. Fragment znakomitego FAQ C ++ :

„Inline wirtualne wywołanie może być wbudowane tylko wtedy, gdy kompilator zna„ dokładną klasę ”obiektu, który jest celem wywołania funkcji wirtualnej. Może się to zdarzyć tylko wtedy, gdy kompilator ma rzeczywisty obiekt, a nie wskaźnik lub odniesienie do obiektu. Tj. albo z obiektem lokalnym, obiektem globalnym / statycznym lub w pełni zawartym w obiekcie złożonym. "


7
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 wbudować.
ostry

6
Inną sytuacją, w której myślę, że może się zdarzyć, jest wywołanie metody, na przykład jako this-> Temp :: myVirtualFunction () - takie wywołanie pomija rozdzielczość tabeli wirtualnej, a funkcja powinna być wstawiana bez problemów - dlaczego i jeśli ' Chciałbym to zrobić to inny temat :)
RnR

5
@RnR. Nie jest konieczne posiadanie „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ź).
Richard Corden

2
sharptooth - prawda, ale AFAIK dotyczy to wszystkich funkcji wbudowanych, a nie tylko funkcji wirtualnych.
Colen

2
void f (const Base & lhs, const Base & rhs) {} ------ W implementacji funkcji nigdy nie wiadomo, na co wskazuje lewa i prawa oś aż do uruchomienia.
Baiyan Huang

72

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:

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.
}

Nie udało mi się go umieścić w VS 2017.
Yola,

1
Myślę, że to nie działa w ten sposób. Wywołanie foo () przez wskaźnik / referencję typu A nigdy nie może zostać wstawione. Wywołanie b.foo () powinno pozwolić na wstawianie. Chyba że sugerujesz, że kompilator już wie, że jest to typ B, ponieważ zna poprzednią linię. Ale to nie jest typowe użycie.
Jeffrey Faust,

Na przykład porównaj wygenerowany kod dla bar i bas tutaj: godbolt.org/g/xy3rNh
Jeffrey Faust

@JeffreyFaust Nie ma powodu, dla którego informacje nie powinny być rozpowszechniane, prawda? I iccwydaje się, że to robi, zgodnie z tym linkiem.
Alexey Romanov

Kompilatory @AlexeyRomanov mają swobodę optymalizacji wykraczającej poza standard i na pewno tak! W prostych przypadkach, takich jak powyżej, kompilator może znać typ i przeprowadzić tę optymalizację. Rzeczy rzadko są tak proste i nie jest typowe, aby móc określić rzeczywisty typ zmiennej polimorficznej w czasie kompilacji. Myślę, że OP dba o „ogólnie”, a nie o te szczególne przypadki.
Jeffrey Faust

37

Jest jedna kategoria funkcji wirtualnych, w których nadal warto mieć je wbudowane. Rozważ 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
}

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.


23
Należy jednak mieć świadomość, że puste nawiasy klamrowe nie zawsze oznaczają, że destruktor nic nie robi. Destruktory domyślnie niszczą każdy obiekt składowy w klasie, więc jeśli masz kilka wektorów w klasie bazowej, może to wymagać sporo pracy w tych pustych nawiasach klamrowych!
Philip

14

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-Alub 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ł virtualprzede 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: Tylko inlinefunkcje 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.


11

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ą Fi klasami pochodnymi Derived1oraz Derived2:

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

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

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

Hipotetyczne wezwanie b->F();(z btypem Base*) jest oczywiście wirtualne. Ale ty (lub kompilator ...) możesz przepisać to w ten sposób (przypuśćmy, że typeofjest to typeidfunkcja podobna do-, która zwraca wartość, której można użyć w a 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)
}

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 prostu Derived1):

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

Czy są to kompilatory, które to robią? Czy to tylko spekulacje? Przepraszam, jeśli jestem przesadnie sceptyczny, ale twój ton w powyższym opisie brzmi trochę jak - „całkowicie by to zrobili!”, Co różni się od „niektórzy kompilatorzy to robią”.
Alex Meiburg

Tak, Graal wykonuje polimorficzne inlining (również dla kodu bitowego LLVM przez Sulong)
CAFxX


3

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 .


2
W przypadku kompilatorów, które działają tylko na pojedynczych jednostkach tłumaczeniowych, mogą wbudowywać tylko niejawnie funkcje, dla których mają definicję. Funkcja może być zdefiniowana w wielu jednostkach tłumaczeniowych tylko wtedy, gdy zostanie umieszczona w tekście. „inline” jest czymś więcej niż wskazówką i może znacznie poprawić wydajność w przypadku kompilacji g ++ / makefile.
Richard Corden

3

Deklarowane wbudowane funkcje wirtualne są wstawiane, gdy są wywoływane przez obiekty i ignorowane, gdy są wywoływane za pomocą wskaźnika lub referencji.


1

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.


1

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).


1
Kiedy wywołujesz metodę klasy bazowej z tej samej lub pochodnej klasy, wywołanie jest jednoznaczne i niewirtualne
ostry

1
@sharptooth: ale wtedy byłaby to niewirtualna metoda wbudowana. Kompilator może wbudować funkcje, o które go nie poprosisz, i prawdopodobnie wie lepiej, kiedy wstawić je, a kiedy nie. Niech zdecyduje.
David Rodríguez - dribeas

1
@dribeas: Tak, dokładnie 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 jest wykonywane wirtualnie, a nie dla dokładnej klasy.
ostry

Uważam, że to nonsens. Każda funkcja może być zawsze wbudowana, bez względu na to, jak duża jest i czy jest wirtualna, czy nie. To zależy od tego, jak został napisany kompilator. Jeśli się nie zgadzasz, spodziewam się, że Twój kompilator również nie może wygenerować kodu niewymienionego. To znaczy: kompilator może zawierać kod, który podczas wykonywania testów warunków, których nie mógł rozwiązać w czasie kompilacji. To tak, jak nowoczesne kompilatory mogą rozwiązywać wartości stałe / redukować wyrażenia nummeryczne w czasie kompilacji. Jeśli funkcja / metoda nie jest wstawiona, nie oznacza to, że nie można jej wstawić.

1

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.


Która wersja g ++ nie będzie kompilować wirtualnych inline?
Thomas L Holaday

Hm. 4.1.1, który tu mam, wydaje się być szczęśliwy. Po raz pierwszy napotkałem problemy z tą bazą kodów przy użyciu wersji 4.0.x. Chyba moje informacje są nieaktualne, edytowane.
moonshadow

0

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.


0

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.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.