14.16 — Konwersja konstruktorów i jawne słowo kluczowe

W lekcji 10.1 -- Niejawna konwersja typów, wprowadziliśmy konwersję typów i koncepcję niejawnej konwersji typów, gdzie kompilator niejawnie przekonwertuje wartość jednego typu na wartość innego typu, jeśli taka konwersja istnieje.

Pozwala nam to robić następujące rzeczy:

#include <iostream>

void printDouble(double d) // has a double parameter
{
    std::cout << d;
}

int main()
{
    printDouble(5); // we're supplying an int argument

    return 0;
}

W powyższym przykładzie nasza printDouble funkcja ma double parametr, ale przekazujemy argument typu int. Ponieważ typ parametru i typ argumentu nie są zgodne, kompilator sprawdzi, czy może niejawnie przekonwertować typ argumentu na typ parametru. W tym przypadku, korzystając z reguł konwersji numerycznej, wartość int 5 zostanie przekonwertowana na wartość double 5.0 a ponieważ przekazujemy wartość, parametr d zostanie kopiowany inicjowany tą wartością.

Konwersje definiowane przez użytkownika

Rozważmy teraz następujący podobny przykład:

#include <iostream>

class Foo
{
private:
    int m_x{};
public:
    Foo(int x)
        : m_x{ x }
    {
    }

    int getX() const { return m_x; }
};

void printFoo(Foo f) // has a Foo parameter
{
    std::cout << f.getX();
}

int main()
{
    printFoo(5); // we're supplying an int argument

    return 0;
}

W tej wersji printFoo posiada parametr Foo ale przekazujemy argument typu int. Ponieważ te typy nie są zgodne, kompilator spróbuje niejawnie przekonwertować obiekt int value 5 do Foo , aby można było wywołać funkcję.

W przeciwieństwie do pierwszego przykładu, w którym oba typy parametrów i argumentów były typami podstawowymi (i dlatego można je konwertować przy użyciu wbudowanych reguł promocji/konwersji numerycznej), w tym przypadku jednym z naszych typów jest typ zdefiniowany w programie. Standard C++ nie ma konkretnych reguł mówiących kompilatorowi, jak konwertować wartości na (lub z) typu zdefiniowanego w programie.

Zamiast tego kompilator sprawdzi, czy zdefiniowaliśmy jakąś funkcję, której może użyć do przeprowadzenia takiej konwersji. Taka funkcja nazywa się konwersją zdefiniowaną przez użytkownika.

Konstruktorami konwertującymi

W powyższym przykładzie kompilator znajdzie funkcję, która pozwoli mu przekonwertować obiekt int value 5 do Foo . Ta funkcja to Foo(int) konstruktor.

Do tego momentu zwykle używaliśmy konstruktorów do jawnego konstruowania obiektów:

    Foo x { 5 }; // Explicitly convert int value 5 to a Foo

Zastanów się, co to robi: dostarczamy int wartość (5) i otrzymujemy w zamian Foo obiekt.

W kontekście wywołanie funkcji, staramy się rozwiązać ten sam problem:

    printFoo(5); // Implicitly convert int value 5 into a Foo

Podajemy int wartość (5), a w zamian chcemy Foo obiekt. Konstruktor Foo(int) został zaprojektowany dokładnie w tym celu!

Więc w tym przypadku, gdy wywoływany jest printFoo(5) , parametr f jest inicjalizowany kopiowaniem przy użyciu Foo(int) konstruktora z 5 jako argumentem!

Na marginesie…

Przed C++17, po wywołaniu printFoo(5) 5 jest niejawnie konwertowane na tymczasowe Foo za pomocą konstruktora Foo(int) . Ta tymczasowa Foo jest następnie kopiowana w parametrze f.

W C++ 17 i nowszych kopia jest obowiązkowo pomijana. Parametr f jest kopiowany z wartością 5 i nie jest wymagane wywołanie konstruktora kopiującego (i będzie działać nawet jeśli konstruktor kopiujący zostanie usunięty).

Konstruktor, którego można użyć do wykonania niejawnej konwersji, nazywa się konstruktorem konwertującym. Domyślnie wszystkie konstruktory konwertują konstruktory.

Można zastosować tylko jedną konwersję zdefiniowaną przez użytkownika

Teraz rozważ następujące kwestie przykład:

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

class Employee
{
private:
    std::string m_name{};

public:
    Employee(std::string_view name)
        : m_name{ name }
    {
    }

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

void printEmployee(Employee e) // has an Employee parameter
{
    std::cout << e.getName();
}

int main()
{
    printEmployee("Joe"); // we're supplying an string literal argument

    return 0;
}

W tej wersji zamieniliśmy naszą Foo klasę na Employee . printEmployee zawierającą parametr Employee i przekazujemy literał łańcuchowy w stylu C. Mamy też konstruktor konwertujący: Employee(std::string_view).

Możesz być zaskoczony, gdy zauważysz, że ta wersja się nie kompiluje. Powód jest prosty: do wykonania konwersji niejawnej można zastosować tylko jedną konwersję zdefiniowaną przez użytkownika, a w tym przykładzie wymagane są dwie. Najpierw nasz literał łańcuchowy w stylu C musi zostać przekonwertowany na std::string_view (przy użyciu std::string_view konstruktora konwertującego), a następnie nasz std::string_view musi zostać przekonwertowany na Employee (za pomocą słowa kluczowego Employee(std::string_view) konstruktor konwertujący).

Są dwa sposoby, aby ten przykład zadziałał:

  1. Użyj std::string_view literał:
int main()
{
    using namespace std::literals;
    printEmployee( "Joe"sv); // now a std::string_view literal

    return 0;
}

Działa to, ponieważ wymagana jest teraz tylko jedna konwersja zdefiniowana przez użytkownika (z std::string_view Do Employee).

  1. Jawnie skonstruuj Employee zamiast pośrednio je tworzyć:
int main()
{
    printEmployee(Employee{ "Joe" });

    return 0;
}

Działa to również, ponieważ wymagana jest teraz tylko jedna konwersja zdefiniowana przez użytkownika (z literału ciągu na std::string_view używany do inicjalizacji Employee obiekt). Przekazywanie naszej jawnie skonstruowanej Employee obiektu do funkcji nie wymaga drugiej konwersji.

Ten ostatni przykład przedstawia użyteczną technikę: przekształcenie ukrytej konwersji w jawną definicję jest banalne. Więcej przykładów tego zobaczymy w dalszej części tej lekcji.

Kluczowa informacja

Niejawną konwersję można w prosty sposób przekształcić w jawną definicję za pomocą bezpośredniej inicjalizacji listy (lub bezpośredniej inicjalizacji).

Podczas konwersji konstruktory popełniają błąd

Rozważ następujący program:

#include <iostream>

class Dollars
{
private:
    int m_dollars{};

public:
    Dollars(int d)
        : m_dollars{ d }
    {
    }

    int getDollars() const { return m_dollars; }
};

void print(Dollars d)
{
    std::cout << "$" << d.getDollars();
}

int main()
{
    print(5);

    return 0;
}

Kiedy wywołujemy print(5), instrukcja Dollars(int) Konstruktor konwertujący zostanie użyty do konwersji 5 do Dollars obiekt. Zatem ten program wypisuje:

$5

Chociaż mógł taki być zamiar dzwoniącego, trudno to stwierdzić, ponieważ dzwoniący nie dał żadnych wskazówek, że rzeczywiście tego chciał. Jest całkiem możliwe, że dzwoniący założył, że to zostanie wydrukowane 5i nie spodziewałem się, że kompilator dyskretnie i niejawnie przekonwertuje nasz plik int wartość do A Dollars obiekt, aby mógł spełnić to wywołanie funkcji.

Chociaż ten przykład jest trywialny, w większym i bardziej złożonym programie dość łatwo jest zaskoczyć się, gdy kompilator wykona jakąś niejawną konwersję, której się nie spodziewałeś, co spowoduje nieoczekiwane zachowanie w czasie wykonywania.

Byłoby lepiej, gdyby nasze print(Dollars) funkcję można wywołać tylko za pomocą a Dollars obiekt, a nie jakakolwiek wartość, którą można niejawnie przekonwertować na a Dollars (szczególnie typ podstawowy, taki jak int). Zmniejszyłoby to możliwość wystąpienia niezamierzonych błędów.

Wyraźne słowo kluczowe

Aby rozwiązać takie problemy, możemy użyć metody jawnego słowo kluczowe, aby poinformować kompilator, że konstruktor nie powinien być używany jako konstruktor konwertujący.

Tworzenie konstruktora explicit ma dwie istotne konsekwencje:

  • Nie można użyć jawnego konstruktora do inicjalizacji kopiowania ani inicjalizacji listy kopiowania.
  • Jawnego konstruktora nie można używać do wykonywania niejawnych konwersji (ponieważ używa to inicjalizacji kopiowania lub inicjalizacji listy kopiowania).

Zaktualizujmy Dollars(int) konstruktor z poprzedniego przykładu, aby był konstruktorem jawnym:

 #include <iostream>

class Dollars
{
private:
    int m_dollars{};

public:
    explicit Dollars(int d) // now explicit
        : m_dollars{ d }
    {
    }

    int getDollars() const { return m_dollars; }
};

void print(Dollars d)
{
    std::cout << "$" << d.getDollars();
}

int main()
{
    print(5); // compilation error because Dollars(int) is explicit

    return 0;
}

Ponieważ kompilator nie może już używać Dollars(int) jako konstruktor konwertujący nie może znaleźć sposobu na konwersję 5 do Dollars. W rezultacie wygeneruje błąd kompilacji.

W przypadku konstruktorów z oddzielną deklaracją (wewnątrz klasy) i definicją (poza klasą) metoda explicit słowo kluczowe jest używane tylko w deklaracji.

Jawnych konstruktorów można używać do bezpośredniej i bezpośredniej inicjalizacji listy

Do bezpośredniej i bezpośredniej inicjalizacji listy nadal można używać jawnego konstruktora:

// Assume Dollars(int) is explicit
int main()
{
    Dollars d1(5); // ok
    Dollars d2{5}; // ok
}

Wróćmy teraz do naszego poprzedniego przykładu, w którym zrobiliśmy nasz Dollars(int) konstruktor jawny, dlatego następujące wygenerowało błąd kompilacji:

    print(5); // compilation error because Dollars(int) is explicit

A co jeśli rzeczywiście będziemy chcieli zadzwonić print() z int wartości 5 ale konstruktor jest jawny? Rozwiązanie jest proste: zamiast pośredniej konwersji kompilatora 5 do Dollars które można przekazać print(), możemy wyraźnie zdefiniować Dollars sprzeciwiamy się sobie:

    print(Dollars{5}); // ok: explicitly create a Dollars

Jest to dozwolone, ponieważ nadal możemy używać jawnych konstruktorów do wyświetlania obiektów inicjujących. A ponieważ teraz wyraźnie skonstruowaliśmy a Dollars, typ argumentu odpowiada typowi parametru, więc nie jest wymagana żadna konwersja!

To nie tylko kompiluje i uruchamia, ale także lepiej dokumentuje nasze intencje, ponieważ wyraźnie wskazuje na fakt, że chcieliśmy wywołać tę funkcję z Dollars obiektem.

Zauważ to static_cast zwraca obiekt, który jest inicjowany bezpośrednio, więc podczas konwersji będzie uwzględniał jawne konstruktory:

    print(static_cast<Dollars>(5)); // ok: static_cast will use explicit constructors

Zwróć według wartości i jawne konstruktory

Gdy zwrócimy wartość z funkcji, jeśli ta wartość nie pasuje do typu zwracanego przez funkcję, nastąpi niejawna konwersja. Podobnie jak w przypadku przekazywania wartości, w takich konwersjach nie można używać jawnych konstruktorów.

Następujące programy pokazują kilka odmian wartości zwracanych i ich wyników:

#include <iostream>

class Foo
{
public:
    explicit Foo() // note: explicit (just for sake of example)
    {
    }

    explicit Foo(int x) // note: explicit
    {
    }
};

Foo getFoo()
{
    // explicit Foo() cases
    return Foo{ };   // ok
    return { };      // error: can't implicitly convert initializer list to Foo

    // explicit Foo(int) cases
    return 5;        // error: can't implicitly convert int to Foo
    return Foo{ 5 }; // ok
    return { 5 };    // error: can't implicitly convert initializer list to Foo
}

int main()
{
    return 0;
}

Być może, co zaskakujące, return { 5 } jest uważany za konwersję.

Najlepsze praktyki dotyczące stosowania explicit

Współczesną najlepszą praktyką jest tworzenie dowolnego konstruktora, który domyślnie akceptuje pojedynczy argument explicit . Obejmuje to konstruktory z wieloma parametrami, z których większość lub wszystkie mają wartości domyślne. Uniemożliwi to kompilatorowi użycie tego konstruktora do niejawnych konwersji. Jeśli wymagana jest niejawna konwersja, brane będą pod uwagę tylko niejawne konstruktory. Jeśli nie zostanie znaleziony żaden niejawny konstruktor do wykonania konwersji, kompilator popełni błąd.

Jeśli taka konwersja jest rzeczywiście pożądana w konkretnym przypadku, przekonwertowanie ukrytej konwersji na jawną definicję przy użyciu bezpośredniej inicjalizacji listy jest trywialne.

Następujące nie powinny wyjaśnione wyraźnie:

  • Kopiuj (i przenoś) konstruktory (ponieważ nie dokonują one konwersji).

Następujące zwykle nie są jako wyraźnie określone:

  • Konstruktory domyślne bez parametrów (ponieważ służą one tylko do konwersji {} na obiekt domyślny, a nie coś, co zwykle musimy ograniczać).
  • Konstruktory akceptujące tylko wiele argumentów (ponieważ zazwyczaj tak nie jest) i tak jest kandydatem do konwersji).

Jeśli jednak wolisz, powyższe można oznaczyć jako jawne, aby zapobiec niejawnym konwersjom z listami pustymi i wieloargumentowymi.

Następujące powinny być zwykle wyjaśnione wyraźnie:

  • Konstruktory przyjmujące pojedynczy argument.

Są pewne sytuacje, w których tak się dzieje sensowne jest, aby konstruktor jednoargumentowy był niejawny. Może to być przydatne, gdy spełnione są wszystkie poniższe warunki:

  • Skonstruowany obiekt jest semantycznie równoważny wartości argumentu.
  • Konwersja jest wydajna.

Na przykład std::string_view konstruktor, który akceptuje argument w postaci łańcucha w stylu C, nie jest jawny, ponieważ jest mało prawdopodobne, aby istniał przypadek, w którym nie bylibyśmy w porządku, gdyby ciąg w stylu C był traktowany jako a std::string_view zamiast tego. I odwrotnie, konstruktor std::string przyjmujący std::string_view jest oznaczony jako jawny, ponieważ chociaż wartość std::string jest semantycznie równoważna wartości std::string_view , konstruowanie a std::string nie jest wydajne.

Najlepsza praktyka

Utwórz dowolny konstruktor, który domyślnie akceptuje pojedynczy argument explicit . Jeśli niejawna konwersja między typami jest zarówno semantycznie równoważna, jak i wydajna, możesz rozważyć uczynienie konstruktora niejawnym.

Nie jawnie kopiuj ani nie przenoś konstruktorów, ponieważ nie wykonują one konwersji.

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