Czysta funkcja wirtualna z implementacją

176

Moje podstawowe zrozumienie jest takie, że nie ma implementacji czystej funkcji wirtualnej, jednak powiedziano mi, że może istnieć implementacja czystej funkcji wirtualnej.

class A {
public:
    virtual void f() = 0;
};

void A::f() {
    cout<<"Test"<<endl;
}

Czy powyższy kod jest w porządku?

Jaki jest cel uczynienia z niej czystej funkcji wirtualnej z implementacją?

skydoor
źródło

Odpowiedzi:

215

Czysta virtualfunkcja musi zostać zaimplementowana w typie pochodnym, który zostanie bezpośrednio utworzony, jednak typ podstawowy nadal może definiować implementację. Klasa pochodna może jawnie wywołać implementację klasy bazowej (jeśli pozwalają na to uprawnienia dostępu) przy użyciu nazwy o pełnym zakresie (przez wywołanie A::f()w Twoim przykładzie - jeśli A::f()były publiclub protected). Coś jak:

class B : public A {

    virtual void f() {
        // class B doesn't have anything special to do for f()
        //  so we'll call A's

        // note that A's declaration of f() would have to be public 
        //  or protected to avoid a compile time problem

        A::f();
    }

};

Przypadek użycia, który przychodzi mi do głowy, to mniej lub bardziej rozsądne zachowanie domyślne, ale projektant klas chce, aby takie zachowanie domyślne było wywoływane tylko jawnie. Może być również tak, że chcesz, aby klasy pochodne zawsze wykonywały swoją własną pracę, ale mogły również wywoływać wspólny zestaw funkcji.

Zwróć uwagę, że chociaż język na to zezwala, nie jest to coś, co jest powszechnie używane (a fakt, że można to zrobić, wydaje się zaskakiwać większość programistów C ++, nawet tych doświadczonych).

Michael Burr
źródło
1
Zapomniałeś dodać, dlaczego zaskakuje to programistów: dzieje się tak, ponieważ definiowanie inline jest standardowo zabronione. Definicje czystych metod wirtualnych muszą być deported. (w formacie .inl lub .cpp, aby zapoznać się z typowymi praktykami nazywania plików).
v.oddou,
więc ta metoda wywoływania jest taka sama jak wywołanie elementu członkowskiego metody statycznej. Jakaś metoda klasowa w Javie.
Sany Liew
2
„rzadko używane” == zła praktyka? Szukałem dokładnie tego samego zachowania, próbując zaimplementować NVI. NVI wydaje mi się dobrą praktyką.
Saskia,
5
Warto zauważyć, że uczynienie A :: f () czystym oznacza, że ​​B musi zaimplementować f () (w przeciwnym razie B byłby abstrakcyjny i nieunikniony). I jak wskazuje @MichaelBurr, zapewnienie implementacji dla A :: f () oznacza, że ​​B może jej użyć do zdefiniowania f ().
Fearless_fool
2
IIRC, Scot Meyer ma doskonały artykuł na temat przypadku użycia tego pytania w jednej ze swoich klasycznych książek "Bardziej efektywny C ++"
irsis
75

Żeby było jasne, nie rozumiesz, co = 0; po funkcji wirtualnej oznacza.

= 0 oznacza, że ​​klasy pochodne muszą zapewniać implementację, a nie, że klasa bazowa nie może zapewnić implementacji.

W praktyce, gdy oznaczysz funkcję wirtualną jako czystą (= 0), podawanie definicji nie ma większego sensu, ponieważ nigdy nie zostanie ona wywołana, chyba że ktoś zrobi to wyraźnie za pośrednictwem Base :: Function (...) lub jeśli Konstruktor klasy bazowej wywołuje daną funkcję wirtualną.

Terry Mahaffey
źródło
9
To jest niepoprawne. Jeśli wywołasz tę czystą funkcję wirtualną w konstruktorze swojej czystej klasy wirtualnej, zostanie wykonane czyste wywołanie wirtualne. W takim przypadku lepiej mieć implementację.
rmn
@rmn, Tak, masz rację co do wirtualnych wywołań w konstruktorach. Zaktualizowałem odpowiedź. Miejmy nadzieję, że wszyscy wiedzą, aby tego nie robić. :)
Terry Mahaffey
3
W rzeczywistości wykonanie podstawowego czystego wywołania z konstruktora skutkuje zachowaniem zdefiniowanym przez implementację. W VC ++ oznacza to awarię _purecall.
Ofek Shilon
@OfekShilon to prawda - miałbym ochotę nazwać to również niezdefiniowanym zachowaniem i kandydatem do złej praktyki / refaktoryzacji kodu (tj. Wywoływania metod wirtualnych wewnątrz konstruktora). Myślę, że ma to związek ze spójnością wirtualnej tabeli, która może nie być przygotowana do kierowania do treści poprawnej implementacji.
teodron
1
W konstruktorach i destruktorach funkcje wirtualne nie są wirtualne.
Jesper Juhl
20

Zaletą tego jest to, że wymusza typy pochodne, aby nadal zastępowały metodę, ale także zapewnia domyślną lub addytywną implementację.

JaredPar
źródło
1
Dlaczego miałbym chcieć wymusić, jeśli istnieje domyślna implementacja? To brzmi jak normalne funkcje wirtualne. Gdyby to była zwykła funkcja wirtualna, mogę albo nadpisać, a jeśli nie, to zostanie dostarczona domyślna implementacja (implementacja bazy).
StackExchange123
19

Jeśli masz kod, który powinien być wykonywany przez klasę pochodną, ​​ale nie chcesz, aby był wykonywany bezpośrednio - i chcesz wymusić jego zastąpienie.

Twój kod jest poprawny, chociaż w sumie nie jest to często używana funkcja i zwykle widziana tylko podczas próby zdefiniowania czystego wirtualnego destruktora - w takim przypadku musisz zapewnić implementację. Zabawne jest to, że skoro wyprowadzisz z tej klasy, nie musisz nadpisywać destruktora.

Stąd jedynym rozsądnym zastosowaniem czystych funkcji wirtualnych jest określenie czystego wirtualnego destruktora jako „nieostatecznego” słowa kluczowego.

Poniższy kod jest zaskakująco poprawny:

class Base {
public:
  virtual ~Base() = 0;
};

Base::~Base() {}

class Derived : public Base {};

int main() { 
  // Base b; -- compile error
  Derived d; 
}
Kornel Kisielewicz
źródło
1
Destruktory klasy bazowej są zawsze nazywane tak czy inaczej, wirtualne lub nie i czyste lub nie; w przypadku innych funkcji nie można zagwarantować, że nadpisująca funkcja wirtualna wywoła implementację klasy bazowej niezależnie od tego, czy wersja klasy bazowej jest czysta, czy nie.
CB Bailey,
1
Ten kod jest zły. Musisz zdefiniować dtor poza definicją klasy z powodu dziwactwa składniowego języka.
@Roger: dzięki, to mi pomogło - to jest kod, którego używałem, dobrze kompiluje się pod MSVC, ale myślę, że nie byłby przenośny.
Kornel Kisielewicz
4

Tak, to jest poprawne. W naszym przykładzie klasy, które pochodzą od A, dziedziczą zarówno interfejs f (), jak i domyślną implementację. Ale wymusza się na klasach pochodnych implementację metody f () (nawet jeśli jest to tylko wywołanie domyślnej implementacji dostarczonej przez A).

Scott Meyers omawia to w Effective C ++ (2nd Edition) Punkt 36 Rozróżnienie między dziedziczeniem interfejsu a dziedziczeniem implementacji. Numer pozycji mógł ulec zmianie w najnowszym wydaniu.

Yukiko
źródło
4

Czyste funkcje wirtualne z treścią lub bez niej oznaczają po prostu, że typy pochodne muszą zapewniać własną implementację.

Czyste treści funkcji wirtualnych w klasie bazowej są przydatne, jeśli klasy pochodne chcą wywołać implementację klasy bazowej.

Brian R. Bondy
źródło
2

Funkcja „virtual void foo () = 0;” składnia nie oznacza, że ​​nie możesz zaimplementować foo () w bieżącej klasie, możesz. Nie oznacza to również, że musisz zaimplementować go w klasach pochodnych . Zanim mnie uderzysz, przyjrzyjmy się Diamentowemu Problemowi: (pamiętaj, że ukryty kod).

class A
{
public: 
    virtual void foo()=0;
    virtual void bar();
}

class B : public virtual A
{
public:
    void foo() { bar(); }
}

class C : public virtual A
{
public:
    void bar();
}

class D : public B, public C
{}

int main(int argc, const char* argv[])
{
    A* obj = new D();
    **obj->foo();**
    return 0;
}

Teraz wywołanie obj-> foo () spowoduje B :: foo (), a następnie C :: bar ().

Widzisz ... czyste metody wirtualne nie muszą być implementowane w klasach pochodnych (foo () nie ma implementacji w klasie C - kompilator skompiluje) W C ++ jest wiele luk.

Mam nadzieję, że mógłbym pomóc :-)

Nir Hedvat
źródło
5
Nie musi być implementowany we WSZYSTKICH klasach pochodnych, ale MUSI mieć implementację we wszystkich klasach pochodnych, które mają zostać utworzone. W Cswoim przykładzie nie można utworzyć wystąpienia obiektu typu . Możesz utworzyć wystąpienie obiektu typu, Dponieważ pobiera on swoją implementację foofrom B.
YoungJohn
0

Jednym z ważnych przypadków użycia czystej metody wirtualnej z treścią implementacji jest sytuacja, gdy chcesz mieć klasę abstrakcyjną, ale nie masz w klasie żadnych odpowiednich metod, aby uczynić ją czystą wirtualną. W takim przypadku możesz uczynić destruktor klasy czystym wirtualnym i umieścić w tym celu żądaną implementację (nawet pustą treść). Jako przykład:

class Foo
{
   virtual ~Foo() = 0;
   void bar1() {}
   void bar2(int x) {}
   // other methods
};

Foo::~Foo()
{
}

Ta technika sprawia, że Fooklasa jest abstrakcyjna iw rezultacie nie można jej bezpośrednio utworzyć. Jednocześnie nie dodałeś dodatkowej, czystej metody wirtualnej, aby uczynić Fooklasę abstrakcyjną.

Gupta
źródło