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 doubleMoneyzdefiniowany 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_balancemetody 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_balancezwróć 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 Moneyelementu , 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 Moneytyp 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_balancejest 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 Moneyzdefiniowanego 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.