25,10 — Rzucanie dynamiczne

Dawno na lekcji 10.6 -- Jawna konwersja typów (casting) i static_cast, sprawdziliśmy koncepcję rzutowania i użycie static_cast do konwersji zmiennych z jednego typu na inny.

W tej lekcji będziemy kontynuować badanie innego typu rzutowania: dynamic_cast.

Potrzeba dynamic_cast

W przypadku polimorfizmu często spotykasz przypadki, w których masz wskaźnik do bazy klasy, ale chcesz uzyskać dostęp do pewnych informacji, które istnieją tylko w klasie pochodnej.

Rozważ następujący (nieco wymyślony) program:

#include <iostream>
#include <string>
#include <string_view>

class Base
{
protected:
	int m_value{};

public:
	Base(int value)
		: m_value{value}
	{
	}
	
	virtual ~Base() = default;
};

class Derived : public Base
{
protected:
	std::string m_name{};

public:
	Derived(int value, std::string_view name)
		: Base{value}, m_name{name}
	{
	}

	const std::string& getName() const { return m_name; }
};

Base* getObject(bool returnDerived)
{
	if (returnDerived)
		return new Derived{1, "Apple"};
	else
		return new Base{2};
}

int main()
{
	Base* b{ getObject(true) };

	// how do we print the Derived object's name here, having only a Base pointer?

	delete b;

	return 0;
}

W tym programie funkcja getObject() zawsze zwraca wskaźnik bazowy, ale wskaźnik ten może wskazywać albo obiekt bazowy, albo obiekt pochodny. W przypadku, gdy wskaźnik Base faktycznie wskazuje na obiekt Derived, jak wywołamy Derived::getName()?

Jednym ze sposobów byłoby dodanie do Base funkcji wirtualnej o nazwie getName() (abyśmy mogli wywołać ją ze wskaźnikiem/odniesieniem Base i dynamicznie przekształcić ją w Derived::getName()). Ale co zwróciłaby ta funkcja, gdyby została wywołana ze wskaźnikiem/odniesieniem Base, które faktycznie wskazywało na obiekt Base? Tak naprawdę nie ma żadnej wartości, która miałaby sens. Co więcej, zanieczyszczalibyśmy naszą klasę Base rzeczami, którymi tak naprawdę powinna się zajmować wyłącznie klasa Derived.

Wiemy, że C++ domyślnie pozwoli ci przekonwertować wskaźnik Derived na wskaźnik Base (w rzeczywistości funkcja getObject() właśnie to robi). Proces ten nazywany jest czasem upcastingiem. A co by było, gdyby istniał sposób na przekonwertowanie wskaźnika bazowego z powrotem na wskaźnik pochodny? Wtedy moglibyśmy wywołać funkcję Derived::getName() bezpośrednio przy użyciu tego wskaźnika i nie martwić się w ogóle o rozpoznawanie funkcji wirtualnych.

dynamic_cast

C++ udostępnia operator rzutowania o nazwie dynamic_cast , którego można użyć właśnie w tym celu. Chociaż rzutowanie dynamiczne ma kilka różnych możliwości, zdecydowanie najczęstszym zastosowaniem rzutowania dynamicznego jest konwertowanie wskaźników klasy bazowej na wskaźniki klasy pochodnej. Ten proces nazywa się downcastingiem.

Użycie dynamic_cast działa tak samo jak static_cast. Oto nasz przykład funkcji main() z góry, w której zastosowano dynamic_cast do konwersji naszego wskaźnika bazowego z powrotem na wskaźnik pochodny:

int main()
{
	Base* b{ getObject(true) };

	Derived* d{ dynamic_cast<Derived*>(b) }; // use dynamic cast to convert Base pointer into Derived pointer

	std::cout << "The name of the Derived is: " << d->getName() << '\n';

	delete b;

	return 0;
}

Wypisuje:

The name of the Derived is: Apple

awaria dynamicznego rzutowania

Powyższy przykład działa, ponieważ b faktycznie wskazuje na obiekt pochodny, więc konwersja b na wskaźnik pochodny zakończyła się sukcesem.

Przyjęliśmy jednak dość niebezpieczne założenie: że b wskazuje na obiekt pochodny. Co by było, gdyby b nie wskazywało na obiekt pochodny? Można to łatwo sprawdzić, zmieniając argument getObject() z true na false. W takim przypadku getObject() zwróci wskaźnik Base do obiektu Base. Kiedy spróbujemy przesłać to dynamicznie do obiektu pochodnego, zakończy się to niepowodzeniem, ponieważ nie można dokonać konwersji.

Jeśli dynamic_cast nie powiedzie się, wynikiem konwersji będzie wskaźnik zerowy.

Ponieważ nie sprawdziliśmy wyniku wskaźnika zerowego, uzyskujemy dostęp do d->getName(), która spróbuje wyłuskać wskaźnik zerowy, co prowadzi do niezdefiniowanego zachowania (prawdopodobnie awaria).

Aby zapewnić bezpieczeństwo tego programu, musimy mieć pewność, że wynik rzutowania dynamicznego rzeczywiście się powiódł:

int main()
{
	Base* b{ getObject(true) };

	Derived* d{ dynamic_cast<Derived*>(b) }; // use dynamic cast to convert Base pointer into Derived pointer

	if (d) // make sure d is non-null
		std::cout << "The name of the Derived is: " << d->getName() << '\n';

	delete b;

	return 0;
}

Reguła

Zawsze upewnij się, że rzutowanie dynamiczne rzeczywiście się powiodło, sprawdzając wynik wskaźnika zerowego.

Pamiętaj, że ponieważ dynamic_cast sprawdza spójność w czasie wykonywania (aby zapewnić możliwość przeprowadzenia konwersji), użycie dynamic_cast powoduje spadek wydajności.

Ponadto zwróć uwagę, że istnieje kilka przypadków, w których przesyłanie w dół przy użyciu funkcji dynamic_cast nie będzie działać:

  1. W przypadku dziedziczenia chronionego lub prywatnego.
  2. Dla klas, które nie deklarują ani nie dziedziczą żadnych funkcji wirtualnych (a tym samym nie mają wirtualnej tabeli).
  3. W niektórych przypadkach związanych z wirtualnymi klasami bazowymi (zobacz tę stronę przykład niektórych z tych przypadków i sposób ich rozwiązania).

Dużanie za pomocą static_cast

Okazuje się, że rzutowanie w dół można również wykonać za pomocą static_cast. Główna różnica polega na tym, że static_cast nie sprawdza typu w czasie wykonywania, aby upewnić się, że to, co robisz, ma sens. To sprawia, że ​​używanie static_cast jest szybsze, ale bardziej niebezpieczne. Jeśli rzucisz Bazę* na Pochodny*, „pomyślnie” to się powiedzie, nawet jeśli wskaźnik Bazy nie wskazuje na obiekt Pochodny. Spowoduje to niezdefiniowane zachowanie podczas próby uzyskania dostępu do wynikowego wskaźnika Derived (który w rzeczywistości wskazuje na obiekt Base).

Jeśli masz całkowitą pewność, że wskaźnik, który rzutujesz w dół, powiedzie się, użycie static_cast jest dopuszczalne. Jednym ze sposobów upewnienia się, że wiesz, jaki typ obiektu wskazujesz, jest użycie funkcji wirtualnej. Oto jeden (niezbyt dobry) sposób, aby to zrobić:

#include <iostream>
#include <string>
#include <string_view>

// Class identifier
enum class ClassID
{
	base,
	derived
	// Others can be added here later
};

class Base
{
protected:
	int m_value{};

public:
	Base(int value)
		: m_value{value}
	{
	}

	virtual ~Base() = default;
	virtual ClassID getClassID() const { return ClassID::base; }
};

class Derived : public Base
{
protected:
	std::string m_name{};

public:
	Derived(int value, std::string_view name)
		: Base{value}, m_name{name}
	{
	}

	const std::string& getName() const { return m_name; }
	ClassID getClassID() const override { return ClassID::derived; }

};

Base* getObject(bool bReturnDerived)
{
	if (bReturnDerived)
		return new Derived{1, "Apple"};
	else
		return new Base{2};
}

int main()
{
	Base* b{ getObject(true) };

	if (b->getClassID() == ClassID::derived)
	{
		// We already proved b is pointing to a Derived object, so this should always succeed
		Derived* d{ static_cast<Derived*>(b) };
		std::cout << "The name of the Derived is: " << d->getName() << '\n';
	}

	delete b;

	return 0;
}

Ale jeśli masz zamiar zadać sobie tyle trudu, aby to zaimplementować (i zapłacić koszt wywołania funkcji wirtualnej i przetworzenia wyniku), równie dobrze możesz po prostu użyć dynamic_cast.

Zastanów się także, co by się stało, gdyby naszym obiektem była w rzeczywistości jakaś klasa wywodząca się z Derived (nazwijmy to D2). Powyższe sprawdzenie b->getClassID() == ClassID::derived nie powiedzie się, ponieważ getClassId() zwróci ClassID::D2, co nie jest równe ClassID::derived. Rzutowanie dynamiczne D2 Do Derived powiodłoby się jednak, ponieważ D2 jest Derived!

obsada dynamiczna i odniesienia

Chociaż wszystkie powyższe przykłady pokazują dynamiczne rzutowanie wskaźników (co jest bardziej powszechne), dynamic_cast można również używać z referencjami. Działa to analogicznie do działania dynamic_cast ze wskaźnikami.

#include <iostream>
#include <string>
#include <string_view>

class Base
{
protected:
	int m_value;

public:
	Base(int value)
		: m_value{value}
	{
	}

	virtual ~Base() = default; 
};

class Derived : public Base
{
protected:
	std::string m_name;

public:
	Derived(int value, std::string_view name)
		: Base{value}, m_name{name}
	{
	}

	const std::string& getName() const { return m_name; }
};

int main()
{
	Derived apple{1, "Apple"}; // create an apple
	Base& b{ apple }; // set base reference to object
	Derived& d{ dynamic_cast<Derived&>(b) }; // dynamic cast using a reference instead of a pointer

	std::cout << "The name of the Derived is: " << d.getName() << '\n'; // we can access Derived::getName through d

	return 0;
}

Ponieważ C++ nie ma „odniesienia zerowego”, dynamic_cast nie może zwrócić odniesienia zerowego w przypadku niepowodzenia. Zamiast tego, jeśli dynamic_cast odniesienia nie powiedzie się, zgłaszany jest wyjątek typu std::bad_cast. O wyjątkach porozmawiamy w dalszej części tego samouczka.

dynamic_cast a static_cast

Nowi programiści czasami nie wiedzą, kiedy używać static_cast a kiedy dynamic_cast. Odpowiedź jest dość prosta: użyj static_cast, chyba że używasz downcastingu, w takim przypadku dynamic_cast jest zwykle lepszym wyborem. Jednakże powinieneś także rozważyć całkowite unikanie rzutowania i korzystanie po prostu z funkcji wirtualnych.

Downcasting a funkcje wirtualne

Niektórzy programiści uważają, że dynamic_cast jest zły i wskazuje na zły projekt klasy. Zamiast tego ci programiści mówią, że powinieneś używać funkcji wirtualnych.

Ogólnie rzecz biorąc, korzystanie z funkcji wirtualnej powinieneś jest preferowane zamiast downcastingu. Są jednak chwile, kiedy rzutowanie w dół jest lepszym wyborem:

  • Kiedy nie możesz zmodyfikować klasy bazowej w celu dodania funkcji wirtualnej (np. ponieważ klasa bazowa jest częścią standardowej biblioteki)
  • Gdy potrzebujesz dostępu do czegoś, co jest specyficzne dla klasy pochodnej (np. funkcji dostępu, która istnieje tylko w klasie pochodnej)
  • Gdy dodanie funkcji wirtualnej do klasy bazowej nie ma sensu (np. nie ma odpowiedniej wartości, którą klasa bazowa mogłaby zwrócić). Użycie czystej funkcji wirtualnej może być tutaj opcją, jeśli nie ma potrzeby tworzenia instancji klasy bazowej.

Ostrzeżenie o dynamic_cast i RTTI

Informacje o typie czasu wykonania (RTTI) to funkcja języka C++, która udostępnia informacje o typie danych obiektu w czasie wykonywania. Ta funkcja jest wykorzystywana przez dynamic_cast. Ponieważ RTTI wiąże się z dość znacznym kosztem wydajności przestrzennej, niektóre kompilatory umożliwiają wyłączenie RTTI w ramach optymalizacji. Nie trzeba dodawać, że jeśli to zrobisz, dynamic_cast nie będzie działać poprawnie.

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:  
148 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze