25.11 — Drukowanie klas odziedziczonych za pomocą operatora<<

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 better

Wyzwania 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:

  1. Zakłada się, że żądany wynik może być reprezentowany jako pojedynczy std::string.
  2. 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!

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
63 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze