Rozważmy następujący program korzystający z funkcji wirtualnej:
#include <iostream>
class Base
{
public:
virtual void print() const { std::cout << "Base"; }
};
class Derived : public Base
{
public:
void print() const override { std::cout << "Derived"; }
};
int main()
{
Derived d{};
Base& b{ d };
b.print(); // will call Derived::print()
return 0;
}Do tej pory powinieneś już się pogodzić z faktem, że b.print() wywoła Derived::print() (ponieważ b odwołuje się do obiektu klasy Derived, Base::print() jest funkcją wirtualną, a Derived::print() jest przesłonięciem).
Chociaż wywoływanie takich funkcji członkowskich w celu wykonania wyników jest w porządku, ten styl funkcji nie pasuje dobrze do std::cout:
#include <iostream>
int main()
{
Derived d{};
Base& b{ d };
std::cout << "b is a ";
b.print(); // messy, we have to break our print statement to call this function
std::cout << '\n';
return 0;
}W tej lekcji przyjrzymy się, jak zastąpić operator<< dla klas korzystających z dziedziczenia, abyśmy mogli używać operatora<< zgodnie z oczekiwaniami, w ten sposób:
std::cout << "b is a " << b << '\n'; // much betterWyzwania związane z operatorem<<
Zacznijmy od przeciążenia operatora << w typowy sposób:
#include <iostream>
class Base
{
public:
virtual void print() const { std::cout << "Base"; }
friend std::ostream& operator<<(std::ostream& out, const Base& b)
{
out << "Base";
return out;
}
};
class Derived : public Base
{
public:
void print() const override { std::cout << "Derived"; }
friend std::ostream& operator<<(std::ostream& out, const Derived& d)
{
out << "Derived";
return out;
}
};
int main()
{
Base b{};
std::cout << b << '\n';
Derived d{};
std::cout << d << '\n';
return 0;
}Ponieważ nie ma tu potrzeby rozwiązywania funkcji wirtualnych, program ten działa zgodnie z oczekiwaniami i wypisuje:
Base Derived
Zamiast tego rozważ następującą funkcję main():
int main()
{
Derived d{};
Base& bref{ d };
std::cout << bref << '\n';
return 0;
}Ten program wypisuje:
Base
Chyba nie tego się spodziewaliśmy. Dzieje się tak, ponieważ nasza wersja operatora<< obsługująca obiekty Base nie jest wirtualna, więc std::cout << bref wywołuje wersję operatora<<, która obsługuje obiekty Base, a nie obiekty Derived.
Na tym polega wyzwanie.
Czy możemy uczynić operator<< wirtualnym?
Jeśli problemem jest to, że operator<< nie jest wirtualny, czy nie możemy go po prostu uczynić wirtualnym?
Krótka odpowiedź brzmi: nie. Istnieje wiele powodów.
Po pierwsze, wirtualizować można tylko funkcje składowe — ma to sens, ponieważ tylko klasy mogą dziedziczyć z innych klas i nie ma możliwości przesłonięcia funkcji znajdującej się poza klasą (można przeciążać funkcje niebędące składowymi, ale nie można ich przesłaniać). Ponieważ zazwyczaj implementujemy operator<< jako znajomego, a znajomi nie są uważani za funkcje członkowskie, zaprzyjaźniona wersja operatora<< nie kwalifikuje się do wirtualizacji. (Aby dowiedzieć się, dlaczego wdrażamy operator<< w ten sposób, wróć do lekcji 21.5 — Przeciążanie operatorów przy użyciu funkcji składowych).
Po drugie, nawet gdybyśmy mogli zwirtualizować operator<<, pojawia się problem polegający na tym, że parametry funkcji Base::operator<< i Derived::operator<< różnią się (wersja Base przyjęłaby parametr Base, a wersja Derived parametr Derived). W związku z tym wersja pochodna nie będzie uważana za zastąpienie wersji podstawowej i w związku z tym nie będzie kwalifikować się do rozpoznawania funkcji wirtualnych.
Co więc ma zrobić programista?
Rozwiązanie
Odpowiedź, jak się okazuje, jest zaskakująco prosta.
Najpierw się ustawiliśmy operator<< jak zwykle jako przyjaciel w naszej klasie podstawowej. Ale zamiast mieć operator<< określić, co wydrukować, zamiast tego wywołamy w ten sposób normalną funkcję składową możesz być zwirtualizowany! Ta wirtualna funkcja zajmie się określeniem, co wydrukować dla każdej klasy.
W tym pierwszym rozwiązaniu nasza wirtualna funkcja członkowska (którą nazywamy identify()) zwraca a std::string, który jest drukowany przez Base::operator<<:
#include <iostream>
class Base
{
public:
// Here's our overloaded operator<<
friend std::ostream& operator<<(std::ostream& out, const Base& b)
{
// Call virtual function identify() to get the string to be printed
out << b.identify();
return out;
}
// We'll rely on member function identify() to return the string to be printed
// Because identify() is a normal member function, it can be virtualized
virtual std::string identify() const
{
return "Base";
}
};
class Derived : public Base
{
public:
// Here's our override identify() function to handle the Derived case
std::string identify() const override
{
return "Derived";
}
};
int main()
{
Base b{};
std::cout << b << '\n';
Derived d{};
std::cout << d << '\n'; // note that this works even with no operator<< that explicitly handles Derived objects
Base& bref{ d };
std::cout << bref << '\n';
return 0;
}Spowoduje to wydrukowanie oczekiwanego wyniku:
Base Derived Derived
Przyjrzyjmy się, jak to działa bardziej szczegółowo.
W przypadku Base b, operator<< jest wywoływany z parametrem b odwołując się do obiektu Base. Wywołanie funkcji wirtualnej b.identify() w ten sposób postanawia Base::identify(), która zwraca „Base” do wydrukowania. Nie ma tu nic specjalnego.
W przypadku Derived d, kompilator najpierw sprawdza, czy istnieje plik operator<< który pobiera obiekt Derived. Nie ma takiego, ponieważ go nie zdefiniowaliśmy. Następnie kompilator sprawdza, czy istnieje plik operator<< który pobiera obiekt Base. Tak jest, więc kompilator dokonuje niejawnego przesyłania naszego obiektu pochodnego do obiektu Base& i wywołuje funkcję (mogliśmy to zrobić sami, ale kompilator jest pod tym względem pomocny). Ponieważ parametr b odwołuje się do obiektu pochodnego, wywołania funkcji wirtualnej b.identify() rozwiązuje się do Derived::identify(), która zwraca „Pochodne” do wydrukowania.
Zauważ, że nie musimy definiować operator<< dla każdej klasy pochodnej! Wersja obsługująca obiekty Base działa dobrze zarówno z obiektami Base, jak i dowolną klasą wywodzącą się z Base!
Trzeci przypadek jest połączeniem dwóch pierwszych. Najpierw kompilator dopasowuje zmienną bref do operator<< , która przyjmuje odwołanie do bazy. Ponieważ parametr b odwołuje się do obiektu Derived, b.identify() rozwiązuje się do Derived::identify(), który zwraca „Derived”.
Problem rozwiązany.
Bardziej elastyczne rozwiązanie
Powyższe rozwiązanie działa świetnie, ale ma dwie potencjalne wady:
- Zakłada się, że żądany wynik może być reprezentowany jako pojedynczy std::string.
- Nasz
identify()Funkcja składowa nie ma dostępu do obiektu strumienia.
To drugie jest problematyczne w przypadkach, gdy potrzebujemy obiektu strumienia, na przykład gdy chcemy wydrukować wartość zmiennej składowej, która ma przeciążony operator<<.
Na szczęście modyfikacja powyższego przykładu jest prosta, aby rozwiązać oba te problemy. W poprzedniej wersji funkcja wirtualna identify() zwracała ciąg znaków do wydrukowania przez Base::operator<<. Zamiast tego w tej wersji zdefiniujemy funkcję elementu wirtualnego print() i przypiszemy tej funkcji odpowiedzialność za drukowanie bezpośrednio .
Oto przykład ilustrujący tę ideę:
#include <iostream>
class Base
{
public:
// Here's our overloaded operator<<
friend std::ostream& operator<<(std::ostream& out, const Base& b)
{
// Delegate printing responsibility for printing to virtual member function print()
return b.print(out);
}
// We'll rely on member function print() to do the actual printing
// Because print() is a normal member function, it can be virtualized
virtual std::ostream& print(std::ostream& out) const
{
out << "Base";
return out;
}
};
// Some class or struct with an overloaded operator<<
struct Employee
{
std::string name{};
int id{};
friend std::ostream& operator<<(std::ostream& out, const Employee& e)
{
out << "Employee(" << e.name << ", " << e.id << ")";
return out;
}
};
class Derived : public Base
{
private:
Employee m_e{}; // Derived now has an Employee member
public:
Derived(const Employee& e)
: m_e{ e }
{
}
// Here's our override print() function to handle the Derived case
std::ostream& print(std::ostream& out) const override
{
out << "Derived: ";
// Print the Employee member using the stream object
out << m_e;
return out;
}
};
int main()
{
Base b{};
std::cout << b << '\n';
Derived d{ Employee{"Jim", 4}};
std::cout << d << '\n'; // note that this works even with no operator<< that explicitly handles Derived objects
Base& bref{ d };
std::cout << bref << '\n';
return 0;
}To daje:
Base Derived: Employee(Jim, 4) Derived: Employee(Jim, 4)
W tej wersji Base::operator<< sam nie wykonuje żadnego drukowania. Zamiast tego po prostu wywołuje funkcję elementu wirtualnego print() i przekazuje jej obiekt strumienia. Funkcja print() używa następnie tego obiektu strumienia do własnego drukowania. Base::print() używa obiektu strumienia do drukowania „Base”. Co ciekawsze, Derived::print() używa obiektu strumienia do wydrukowania zarówno „Derived: ”, jak i do wywołania Employee::operator<< w celu wydrukowania wartości elementu m_e. To drugie byłoby trudniejsze w poprzednim przykładzie!

