W lekcji 10.6 -- Jawna konwersja typów (casting) i static_cast, dowiedziałeś się, że C++ pozwala na konwersję jednego typu danych na inny. Poniższy przykład pokazuje, jak int jest konwertowany na double:
int n{ 5 };
auto d{ static_cast<double>(n) }; // int cast to a doubleC++ już wie, jak konwertować pomiędzy wbudowanymi typami danych. Jednak domyślnie C++ nie wie, jak przekonwertować dowolną z klas zdefiniowanych przez program.
W lekcji 14.16 — Konwersja konstruktorów i jawne słowo kluczowe pokazaliśmy, jak możemy użyć konstruktora konwertującego, aby utworzyć obiekt typu klasy z obiektu innego typu. Działa to jednak tylko wtedy, gdy typem docelowym jest typ klasy, który można zmodyfikować w celu dodania takiego konstruktora. A co jeśli tak nie jest?
Przyjrzyj się poniższej klasie:
class Cents
{
private:
int m_cents{};
public:
Cents(int cents=0)
: m_cents{ cents }
{
}
int getCents() const { return m_cents; }
void setCents(int cents) { m_cents = cents; }
};Ta klasa jest całkiem prosta: przechowuje pewną liczbę centów jako liczbę całkowitą i zapewnia funkcje dostępu umożliwiające pobieranie i ustawianie liczby centów. Udostępnia także konstruktor do konwersji int do Cents.
Jeśli możemy przekonwertować int do Cents (za pośrednictwem konstruktora), to możemy również chcieć zapewnić sposób na konwersję Cents z powrotem na int. W niektórych przypadkach może to nie być pożądane, ale tutaj ma to sens.
Nota autora
Wiersz:
Konwersja naszych intów na kilka centów
Użyje konstruktora, który zatwierdza
Oczywiście, centy na int również mają sens
Ale błędy kompilatora i zapobiega.
Aby zezwolić na takie zdarzenia konwersji
Wydajemy kompilatorowi naszą zgodę
A następnie definiujemy, w jaki sposób, dla wszystkich celów
Możemy przekształcać tego typu treści.
Jaka jest więc składnia, która omija
Typ statyczny kompilatora obrona?
Wkrótce szczegółowo opiszemy, jak to możliwe, dlatego
Nie będziesz już trzymany w niepewności.
Jednym z niezbyt dobrych sposobów jest użycie funkcji konwersji. W tym przykładzie używamy funkcji składowej getCents() w celu „przekonwertowania” naszej Cents zmiennej z powrotem na int abyśmy mogli ją wydrukować za pomocą printInt():
#include <iostream>
void printInt(int value)
{
std::cout << value;
}
int main()
{
Cents cents{ 7 };
printInt(cents.getCents()); // print 7
std::cout << '\n';
return 0;
}Chociaż ta funkcja daje pożądany wynik, nie jest to tak naprawdę konwersja, ponieważ kompilator nie zrozumie, że powinien używać tej funkcji do rzutowania lub konwersji niejawnej. Oznacza to również, że jeśli wykonamy dużo Cents Do int konwersji, nasz kod będzie zaśmiecony wywołaniami metody getCents(), co powoduje bałagan.
Co jeszcze możemy zrobić?
Przeciążanie rzutowania
W tym miejscu wchodzi w grę przeciążanie operatorów rzutowania. Takiego rzutowania można używać jawnie (poprzez rzutowanie) lub niejawnie przez kompilator do wykonywania konwersji w razie potrzeby.
Pokażmy, jak przeciążamy rzutowanie, aby zdefiniować konwersję z Cents Do int:
class Cents
{
private:
int m_cents{};
public:
Cents(int cents=0)
: m_cents{ cents }
{
}
// Overloaded int cast
operator int() const { return m_cents; }
int getCents() const { return m_cents; }
void setCents(int cents) { m_cents = cents; }
};W tym celu napisaliśmy nowy przeciążony operator o nazwie operator int(). Zauważ, że pomiędzy operatorem słowa a typem, na który rzutujemy, znajduje się spacja.
Jest kilka rzeczy, na które warto zwrócić uwagę:
- Przeciążone rzuty muszą być niestatycznymi elementami i powinny być
constaby można było ich używać z obiektami stałymi. - Przeciążone rzuty nie mają jawnych parametrów, ponieważ nie ma możliwości przekazania im jawnych argumentów. Nadal mają ukryty
*thisparametr wskazujący na ukryty obiekt (który jest obiektem do konwersji). - Przeciążony rzutowanie nie deklaruje typu zwracanego. Jako typ zwracany używana jest nazwa konwersji (np. int), ponieważ jest to jedyny dozwolony typ zwracany. Zapobiega to redundancji deklaracji.
Teraz w naszym przykładzie możemy wywołać printInt() w ten sposób:
#include <iostream>
int main()
{
Cents cents{ 7 };
printInt(cents); // print 7
std::cout << '\n';
return 0;
}Kompilator najpierw zanotuje parametr funkcji printInt() zawierającą parametr int . Następnie zauważy, że zmienna cents nie jest int. Na koniec sprawdzimy, czy udostępniliśmy sposób na konwersję Cents na int. Ponieważ tak jest, wywoła naszą operator int() funkcję, która zwróci int, a zwrócona int zostanie przekazana do printInt().
Takie typy rzutowania można również wywołać jawnie poprzez static_cast:
std::cout << static_cast<int>(cents);Możesz zapewnić przeciążone rzutowanie typu dla dowolnego typu danych, w tym typów danych zdefiniowanych przez program!
Oto nowa klasa o nazwie Dollars , która zapewnia przeciążoną Cents konwersję:
class Dollars
{
private:
int m_dollars{};
public:
Dollars(int dollars=0)
: m_dollars{ dollars }
{
}
// Allow us to convert Dollars into Cents
operator Cents() const { return Cents{ m_dollars * 100 }; }
};Pozwala nam to przekonwertować Dollars obiekt bezpośrednio na obiekt Cents ! Pozwala to zrobić coś takiego:
#include <iostream>
class Cents
{
private:
int m_cents{};
public:
Cents(int cents=0)
: m_cents{ cents }
{
}
// Overloaded int cast
operator int() const { return m_cents; }
int getCents() const { return m_cents; }
void setCents(int cents) { m_cents = cents; }
};
class Dollars
{
private:
int m_dollars{};
public:
Dollars(int dollars=0)
: m_dollars{ dollars }
{
}
// Allow us to convert Dollars into Cents
operator Cents() const { return Cents { m_dollars * 100 }; }
};
void printCents(Cents cents)
{
std::cout << cents; // cents will be implicitly cast to an int here
}
int main()
{
Dollars dollars{ 9 };
printCents(dollars); // dollars will be implicitly cast to a Cents here
std::cout << '\n';
return 0;
}W rezultacie ten program wypisze wartość:
900
co ma sens, ponieważ 9 dolarów to 900 centów!
Chociaż pokazuje to, że coś takiego jest możliwe, w tym przypadku dodanie konstruktora konwertującego do Dollars (z parametrem typu Cents) jest w rzeczywistości preferowane. Poniżej omówimy dlaczego.
Jawne rzuty typem
Tak jak możemy tworzyć konstruktory explicit tak, aby nie można było ich używać do niejawnych konwersji, możemy również tworzyć nasze przeciążone typy rzutowania explicit z tego samego powodu. Jawne rzutowanie można wywołać jedynie poprzez rzutowanie (np. static_cast) lub poprzez formę bezpośredniej inicjalizacji (nawias lub nawias klamrowy). Nie są one brane pod uwagę podczas inicjalizacji kopiowania.
#include <iostream>
class Cents
{
private:
int m_cents{};
public:
Cents(int cents=0)
: m_cents{ cents }
{
}
explicit operator int() const { return m_cents; } // now marked as explicit
int getCents() const { return m_cents; }
void setCents(int cents) { m_cents = cents; }
};
class Dollars
{
private:
int m_dollars{};
public:
Dollars(int dollars=0)
: m_dollars{ dollars }
{
}
operator Cents() const { return Cents { m_dollars * 100 }; }
};
void printCents(Cents cents)
{
// std::cout << cents; // no longer works because cents won't implicit convert to an int
std::cout << static_cast<int>(cents); // we can use an explicit cast instead
}
int main()
{
Dollars dollars{ 9 };
printCents(dollars); // implicit conversion from Dollars to Cents okay because its not marked as explicit
std::cout << '\n';
return 0;
}Typecasty powinny być generalnie oznaczane jako jawne. Wyjątki można zrobić w przypadkach, gdy konwersja niedrogo konwertuje na podobny typ zdefiniowany przez użytkownika. Nasz Dollars::operator Cents() typecast pozostawiono niejawny, ponieważ nie ma powodu, aby nie pozwalać Dollars używać obiektu gdziekolwiek Cents jest oczekiwany.
Najlepsza praktyka
Tak jak jednoparametrowe konstruktory konwertujące powinny być oznaczone jako jawne, typecasty powinny być oznaczone jako jawne, z wyjątkiem przypadków, gdy typ, na który ma zostać przekonwertowany, jest zasadniczo równoznaczny z miejscem docelowym type.
Kiedy używać konstruktorów konwertujących, a kiedy przeciążonych rzutów typu
Przeciążone rzutowania i konstruktory konwertujące wykonują podobne funkcje:
- Konstruktor konwertujący to funkcja składowa klasy B, która definiuje sposób tworzenia B z A.
- Przeciążony rzutowanie to funkcja składowa klasy typu A, która definiuje sposób konwersji A na B.
W obu przypadkach zaczynamy od A, a kończymy na B. Główna różnica między nimi polega na tym, czy A czy B jest właścicielem sposobu, w jaki zachodzi konwersja.
Ponieważ oba sposoby wymagają zdefiniowania funkcji składowej, można ich używać tylko w przypadku typów klas, które można modyfikować. Jeśli A nie jest typem klasy, który można modyfikować, nie można użyć przeciążonego rzutowania. Jeśli B nie jest typem klasy, który można modyfikować, nie można użyć konstruktora konwertującego. Jeśli żadnego z typów klas nie można modyfikować, zamiast tego będziesz musiał użyć funkcji konwersji niebędącej składową.
W przypadkach, gdy A i B są typami klas, które można modyfikować, możemy użyć jednego z nich. Ale ponieważ potrzebujemy tylko jednego, co powinniśmy preferować?
Ogólnie rzecz biorąc, konstruktor konwertujący powinien być preferowany zamiast przeciążonego rzutowania. Wszystkie pozostałe czynniki pozostają niezmienne, bardziej przejrzyście jest, gdy typ klasy posiada własną konstrukcję, zamiast polegać na innej klasie w celu jej utworzenia i inicjalizacji.
Najlepsza praktyka
Jeśli to możliwe, preferuj konwersję konstruktorów i unikaj przeciążonych rzutowań.
Istnieje kilka przypadków, w których należy zamiast tego użyć przeciążonego rzutowania typów:
- Podczas zapewniania konwersji na typ podstawowy (ponieważ nie można definiować konstruktorów dla tych typów). W najbardziej konwencjonalny sposób służą one do konwersji na
boolw przypadkach, w których użycie obiektu w instrukcji warunkowej ma sens. - Gdy konwersja zwraca referencję lub stałą referencję.
- Podczas zapewniania konwersji na typ, do którego nie można dodawać elementów (np. konwersja do
std::vector, ponieważ nie można definiować konstruktorów dla tych typów albo). - Gdy nie chcesz, aby konstruowany typ wiedział, z jakiego typu jest konwertowany. Może to być pomocne w unikaniu zależności cyklicznych.
Na przykład ostatni punktor, std::string ma konstruktor do utworzenia std::string z a std::string_view. Oznacza to, że <string> musi zawierać <string_view>. Jeżeli std::string_view miał konstruktor, aby utworzyć std::string_view z a std::string, wówczas <string_view> będzie musiał zawierać <string>, a to skutkowałoby kołową zależnością między nagłówkami.
Zamiast tego std::string ma przeciążony rzutowanie typów, które obsługuje konwersję z std::string Do std::string_view (co jest w porządku, ponieważ już tak jest include <string_view>). std::string_view w ogóle nie wie o std::string i dlatego nie musi uwzględniać <string>. W ten sposób unika się zależności cyklicznej.
Gdy konstruktor konwertujący i przeciążony rzutowanie są zdefiniowane dla tej samej konwersji, oba są brane pod uwagę podczas rozwiązywania przeciążenia. W zależności od tego, czy przeciążony rzut jest stały, konwertowany obiekt jest stały i jakiego typu jest rzutowanie używana jest inicjalizacja (kopiowanie a bezpośrednie), można wybrać dowolną funkcję (co może skutkować wybraniem rzutowania zamiast konstruktora konwertującego) lub wynik może być niejednoznaczny (co powoduje błąd kompilacji). Z tego powodu należy unikać definiowania zarówno przeciążonego rzutowania, jak i konstruktora konwertującego, który może obsłużyć tę samą konwersję.
Najlepsza praktyka
Gdy musisz zdefiniować sposób konwersji typu A na typ B:
- Jeśli B jest typem klasy, który możesz modyfikować, preferuj użycie konstruktora konwertującego do utworzenia B z A.
- W przeciwnym razie, jeśli A jest typem klasy, który możesz modyfikować, użyj przeciążonego rzutowania typów, aby przekonwertować A na B.
- W przeciwnym razie użyj funkcji niebędącej składową, aby przekonwertować A na B.

