Konwersje typów zdefiniowane przez użytkownika (C++)
Konwersja generuje nową wartość typu z wartości innego typu. Konwersje standardowe są wbudowane w język C++ i obsługują jego wbudowane typy, a także można tworzyć konwersje zdefiniowane przez użytkownika w celu przeprowadzania konwersji na typy zdefiniowane przez użytkownika lub między nimi.
Konwersje standardowe wykonują konwersje między wbudowanymi typami, między wskaźnikami lub odwołaniami do typów powiązanych z dziedziczeniem, do i ze wskaźników void oraz do wskaźnika o wartości null. Aby uzyskać więcej informacji, zobacz Konwersje standardowe. Konwersje zdefiniowane przez użytkownika wykonują konwersje między typami zdefiniowanymi przez użytkownika lub między typami zdefiniowanymi przez użytkownika i wbudowanymi typami. Można je zaimplementować jako konstruktory konwersji lub jako funkcje konwersji.
Konwersje mogą być jawne — gdy programista wywołuje jeden typ do konwersji na inny, tak jak w inicjalizacji rzutowanej lub bezpośredniej — lub niejawnej — gdy język lub program wywołuje inny typ niż dany przez programistę.
Niejawne konwersje są podejmowane w przypadku:
Argument dostarczony do funkcji nie ma tego samego typu co pasujący parametr.
Wartość zwrócona z funkcji nie ma tego samego typu co zwracany typ funkcji.
Wyrażenie inicjatora nie ma tego samego typu co obiekt, który inicjuje.
Wyrażenie, które steruje instrukcją warunkową, konstrukcją pętli lub przełącznikiem, nie ma typu wyniku wymaganego do kontrolowania.
Operand dostarczony operatorowi nie ma tego samego typu co pasujący parametr operand-. W przypadku operatorów wbudowanych oba operandy muszą mieć ten sam typ i są konwertowane na wspólny typ, który może reprezentować oba te elementy. Aby uzyskać więcej informacji, zobacz Konwersje standardowe. W przypadku operatorów zdefiniowanych przez użytkownika każdy operand musi mieć ten sam typ co pasujący parametr operand-.
Gdy jedna konwersja standardowa nie może ukończyć niejawnej konwersji, kompilator może użyć konwersji zdefiniowanej przez użytkownika, a następnie opcjonalnie przez dodatkową konwersję standardową, aby ją ukończyć.
Jeśli co najmniej dwie konwersje zdefiniowane przez użytkownika, które wykonują tę samą konwersję, są dostępne w lokacji konwersji, konwersja jest określana jako niejednoznaczna. Takie niejednoznaczności są błędem, ponieważ kompilator nie może określić, która z dostępnych konwersji powinna wybrać. Jednak nie jest to błąd tylko do zdefiniowania wielu sposobów przeprowadzania tej samej konwersji, ponieważ zestaw dostępnych konwersji może być inny w różnych lokalizacjach w kodzie źródłowym — na przykład w zależności od tego, które pliki nagłówkowe są zawarte w pliku źródłowym. Tak długo, jak tylko jedna konwersja jest dostępna w lokacji konwersji, nie ma wątpliwości. Istnieje kilka sposobów, na które mogą wystąpić niejednoznaczne konwersje, ale najbardziej typowe są następujące:
Wiele dziedziczenia. Konwersja jest zdefiniowana w więcej niż jednej klasie bazowej.
Niejednoznaczne wywołanie funkcji. Konwersja jest definiowana jako konstruktor konwersji typu docelowego i jako funkcja konwersji typu źródłowego. Aby uzyskać więcej informacji, zobacz Funkcje konwersji.
Zazwyczaj można rozwiązać niejednoznaczność, kwalifikując nazwę zaangażowanego typu w pełni lub wykonując jawne rzutowanie w celu wyjaśnienia intencji.
Zarówno konstruktory konwersji, jak i funkcje konwersji przestrzegają reguł kontroli dostępu do składowych, ale dostępność konwersji jest brana pod uwagę tylko wtedy, gdy można określić jednoznaczną konwersję. Oznacza to, że konwersja może być niejednoznaczna, nawet jeśli poziom dostępu konkurencyjnej konwersji uniemożliwi jego użycie. Aby uzyskać więcej informacji na temat ułatwień dostępu do składowych, zobacz Kontrola dostępu do składowych.
Jawne słowo kluczowe i problemy z niejawną konwersją
Domyślnie podczas tworzenia konwersji zdefiniowanej przez użytkownika kompilator może używać go do przeprowadzania niejawnych konwersji. Czasami jest to to, czego potrzebujesz, ale czasami proste reguły, które kierują kompilatorem w tworzeniu niejawnych konwersji, mogą prowadzić go do akceptowania kodu, do którego nie chcesz.
Jednym z dobrze znanych przykładów niejawnej konwersji, która może powodować problemy, jest konwersja na bool
. Istnieje wiele powodów, dla których można utworzyć typ klasy, który może być używany w kontekście logicznym — na przykład w celu kontrolowania if
instrukcji lub pętli — ale gdy kompilator wykonuje konwersję zdefiniowaną przez użytkownika na wbudowany typ, kompilator może później zastosować dodatkową konwersję standardową. Celem tej dodatkowej standardowej konwersji jest zezwolenie na takie elementy jak podwyższenie poziomu z short
do int
, ale także otwiera drzwi dla mniej oczywistych konwersji — na przykład z bool
do int
, co umożliwia użycie typu klasy w kontekstach liczb całkowitych, których nigdy nie zamierzasz. Ten konkretny problem jest znany jako problem z Sejf wartością logiczną. Ten rodzaj problemu polega na explicit
tym, że słowo kluczowe może pomóc.
Słowo explicit
kluczowe informuje kompilator, że określona konwersja nie może być używana do przeprowadzania niejawnych konwersji. Jeśli chcesz, aby składnia wygody niejawnych konwersji przed explicit
wprowadzeniem słowa kluczowego została wprowadzona, musisz zaakceptować niezamierzone konsekwencje, które niejawna konwersja czasami została utworzona lub użyta mniej wygodne, nazwane funkcje konwersji jako obejście. Teraz, używając słowa kluczowegoexplicit
, można utworzyć wygodne konwersje, których można użyć tylko do wykonywania jawnych rzutów lub bezpośredniej inicjacji, i to nie doprowadzi do rodzaju problemów, które były przykładem Sejf problemu logicznego.
Słowo explicit
kluczowe można zastosować do konstruktorów konwersji od języka C++98 i do funkcji konwersji od języka C++11. Poniższe sekcje zawierają więcej informacji na temat używania słowa kluczowego explicit
.
Konstruktory konwersji
Konstruktory konwersji definiują konwersje z typów zdefiniowanych przez użytkownika lub wbudowanych do typu zdefiniowanego przez użytkownika. W poniższym przykładzie pokazano konstruktor konwersji, który konwertuje typ wbudowany na typ double
Money
zdefiniowany przez użytkownika.
#include <iostream>
class Money
{
public:
Money() : amount{ 0.0 } {};
Money(double _amount) : amount{ _amount } {};
double amount;
};
void display_balance(const Money balance)
{
std::cout << "The balance is: " << balance.amount << std::endl;
}
int main(int argc, char* argv[])
{
Money payable{ 79.99 };
display_balance(payable);
display_balance(49.95);
display_balance(9.99f);
return 0;
}
Zwróć uwagę, że pierwsze wywołanie funkcji display_balance
, które przyjmuje argument typu Money
, nie wymaga konwersji, ponieważ jego argument jest poprawnym typem. Jednak w drugim wywołaniu display_balance
metody wymagana jest konwersja, ponieważ typ argumentu, a double
z wartością 49.95
, nie jest tym, czego oczekuje funkcja. Funkcja nie może używać tej wartości bezpośrednio, ale ponieważ istnieje konwersja z typu argumentu —double
do typu pasującego parametruMoney
— tymczasowa wartość typu Money
jest konstruowana z argumentu i używana do ukończenia wywołania funkcji. W trzecim wywołaniu metody display_balance
zwróć uwagę, że argument nie jest argumentem double
, ale jest zamiast float
niego z wartością 9.99
— a jednak wywołanie funkcji można nadal wykonać, ponieważ kompilator może wykonać standardową konwersję — w tym przypadku z float
do double
— a następnie wykonać konwersję zdefiniowaną przez użytkownika z double
, aby Money
ukończyć wymaganą konwersję.
Deklarowanie konstruktorów konwersji
Następujące reguły dotyczą deklarowania konstruktora konwersji:
Typ docelowy konwersji to typ zdefiniowany przez użytkownika, który jest konstruowany.
Konstruktory konwersji zazwyczaj przyjmują dokładnie jeden argument, który jest typu źródłowego. Jednak konstruktor konwersji może określić dodatkowe parametry, jeśli każdy dodatkowy parametr ma wartość domyślną. Typ źródła pozostaje typem pierwszego parametru.
Konstruktory konwersji, takie jak wszystkie konstruktory, nie określają typu zwracanego. Określenie typu zwracanego w deklaracji jest błędem.
Konstruktory konwersji mogą być jawne.
Konstruktory konwersji jawnej
Deklarując konstruktor konwersji jako explicit
, można go użyć tylko do wykonania bezpośredniej inicjalizacji obiektu lub wykonania jawnego rzutowania. Zapobiega to funkcjom, które akceptują argument typu klasy z niejawnego akceptowania argumentów typu źródłowego konstruktora konwersji i uniemożliwiają kopiowanie typu klasy z wartości typu źródłowego. W poniższym przykładzie pokazano, jak zdefiniować jawny konstruktor konwersji i wpływ na kod, który jest poprawnie sformułowany.
#include <iostream>
class Money
{
public:
Money() : amount{ 0.0 } {};
explicit Money(double _amount) : amount{ _amount } {};
double amount;
};
void display_balance(const Money balance)
{
std::cout << "The balance is: " << balance.amount << std::endl;
}
int main(int argc, char* argv[])
{
Money payable{ 79.99 }; // Legal: direct initialization is explicit.
display_balance(payable); // Legal: no conversion required
display_balance(49.95); // Error: no suitable conversion exists to convert from double to Money.
display_balance((Money)9.99f); // Legal: explicit cast to Money
return 0;
}
W tym przykładzie zwróć uwagę, że nadal można użyć jawnego konstruktora konwersji do wykonania bezpośredniej inicjalizacji payable
. Jeśli zamiast tego należałoby skopiować inicjowanie Money payable = 79.99;
, byłby to błąd. Pierwsze wywołanie metody nie display_balance
ma wpływu, ponieważ argument jest poprawnym typem. Drugie wywołanie display_balance
metody to błąd, ponieważ konstruktor konwersji nie może służyć do przeprowadzania niejawnych konwersji. Trzecie wywołanie metody display_balance
jest legalne ze względu na jawne rzutowanie do Money
elementu , ale zwróć uwagę, że kompilator nadal pomógł ukończyć rzutowanie przez wstawienie niejawnego rzutowania z float
elementu na double
.
Chociaż wygoda zezwalania na niejawne konwersje może być kusząca, może to powodować trudne do znalezienia błędy. Reguła kciuka polega na jawnym ustawieniu wszystkich konstruktorów konwersji, z wyjątkiem sytuacji, gdy na pewno chcesz, aby określona konwersja wystąpiła niejawnie.
Funkcje konwersji
Funkcje konwersji definiują konwersje z typu zdefiniowanego przez użytkownika do innych typów. Te funkcje są czasami określane jako "operatory rzutowania", ponieważ wraz z konstruktorami konwersji są wywoływane, gdy wartość jest rzutowane na inny typ. W poniższym przykładzie pokazano funkcję konwersji, która konwertuje typ zdefiniowany przez użytkownika na Money
typ wbudowany: double
#include <iostream>
class Money
{
public:
Money() : amount{ 0.0 } {};
Money(double _amount) : amount{ _amount } {};
operator double() const { return amount; }
private:
double amount;
};
void display_balance(const Money balance)
{
std::cout << "The balance is: " << balance << std::endl;
}
Zwróć uwagę, że zmienna amount
składowa jest prywatna i że funkcja konwersji publicznej na typ double
jest wprowadzana tylko w celu zwrócenia wartości amount
. W funkcji display_balance
jest wykonywana niejawna konwersja, gdy wartość balance
jest przesyłana strumieniowo do standardowych danych wyjściowych przy użyciu operatora <<
wstawiania strumienia . Ponieważ żaden operator wstawiania strumienia nie jest zdefiniowany dla typu Money
zdefiniowanego przez użytkownika , ale istnieje jeden dla wbudowanego typu double
, kompilator może użyć funkcji konwersji z Money
, aby double
spełnić wymagania operatora wstawiania strumienia.
Funkcje konwersji są dziedziczone przez klasy pochodne. Funkcje konwersji w klasie pochodnej zastępują tylko funkcję konwersji dziedziczonej, gdy konwertują na dokładnie ten sam typ. Na przykład funkcja konwersji zdefiniowana przez użytkownika operatora klasy pochodnej int nie zastępuje ani nawet wpływa na funkcję konwersji zdefiniowaną przez użytkownika operatora klasy bazowej, mimo że konwersje standardowe definiują relację konwersji między int
i short
.
Deklarowanie funkcji konwersji
Następujące reguły dotyczą deklarowania funkcji konwersji:
Typ docelowy konwersji musi być zadeklarowany przed deklaracją funkcji konwersji. Klasy, struktury, wyliczenia i definicje typów nie mogą być deklarowane w deklaracji funkcji konwersji.
operator struct String { char string_storage; }() // illegal
Funkcje konwersji nie przyjmują żadnych argumentów. Określanie dowolnych parametrów w deklaracji jest błędem.
Funkcje konwersji mają typ zwracany określony przez nazwę funkcji konwersji, która jest również nazwą typu docelowego konwersji. Określenie typu zwracanego w deklaracji jest błędem.
Funkcje konwersji mogą być wirtualne.
Funkcje konwersji mogą być jawne.
Jawne funkcje konwersji
Gdy funkcja konwersji jest zadeklarowana jako jawna, może służyć tylko do wykonywania jawnego rzutowania. Zapobiega to funkcjom, które akceptują argument typu docelowego funkcji konwersji z niejawnego akceptowania argumentów typu klasy, i uniemożliwiają wystąpienia typu docelowego inicjowane z wartości typu klasy. W poniższym przykładzie pokazano, jak zdefiniować jawną funkcję konwersji i wpływ na kod, który jest poprawnie sformułowany.
#include <iostream>
class Money
{
public:
Money() : amount{ 0.0 } {};
Money(double _amount) : amount{ _amount } {};
explicit operator double() const { return amount; }
private:
double amount;
};
void display_balance(const Money balance)
{
std::cout << "The balance is: " << (double)balance << std::endl;
}
Tutaj operator funkcji konwersji dwukrotnie został jawny, a jawne rzutowanie do typu double
zostało wprowadzone w funkcji display_balance
w celu przeprowadzenia konwersji. Jeśli to rzutowanie zostanie pominięte, kompilator nie będzie mógł zlokalizować odpowiedniego operatora <<
wstawiania strumienia dla typu Money
i wystąpi błąd.
Opinia
https://aka.ms/ContentUserFeedback.
Dostępne już wkrótce: W 2024 r. będziemy stopniowo wycofywać zgłoszenia z serwisu GitHub jako mechanizm przesyłania opinii na temat zawartości i zastępować go nowym systemem opinii. Aby uzyskać więcej informacji, sprawdź:Prześlij i wyświetl opinię dla