14.13 -- Tymczasowe obiekty klas

Rozważ następujący przykład:

#include <iostream>

int add(int x, int y)
{
    int sum{ x + y }; // stores x + y in a variable
    return sum;       // returns value of that variable
}

int main()
{
    std::cout << add(5, 3) << '\n';

    return 0;
}

W add() funkcja, zmienna sum służy do przechowywania wyniku wyrażenia x + y. Zmienna ta jest następnie oceniana w instrukcji return w celu wygenerowania wartości, która ma zostać zwrócona. Chociaż może to być czasami przydatne do debugowania (abyśmy mogli sprawdzić wartość sum jeśli jest to pożądane), w rzeczywistości sprawia to, że funkcja jest bardziej złożona, niż jest to konieczne, poprzez zdefiniowanie obiektu, który jest następnie używany tylko raz.

W większości przypadków, gdy zmienna jest używana tylko raz, tak naprawdę zmienna nie jest nam potrzebna. Zamiast tego możemy zastąpić wyrażenie użyte do inicjacji zmiennej w miejscu, w którym zmienna zostałaby użyta. Oto add() funkcja przepisana w ten sposób:

#include <iostream>

int add(int x, int y)
{
    return x + y; // just return x + y directly
}

int main()
{
    std::cout << add(5, 3) << '\n';

    return 0;
}

Działa to nie tylko z wartościami zwracanymi, ale także z większością argumentów funkcji. Na przykład zamiast tego:

#include <iostream>

void printValue(int value)
{
    std::cout << value;
}

int main()
{
    int sum{ 5 + 3 };
    printValue(sum);

    return 0;
}

Możemy napisać tak:

#include <iostream>

void printValue(int value)
{
    std::cout << value;
}

int main()
{
    printValue(5 + 3);

    return 0;
}

Zwróć uwagę, o ile dzięki temu nasz kod będzie czystszy. Nie musimy definiować i nadawać nazwy zmiennej. Nie musimy też przeglądać całej funkcji, aby ustalić, czy ta zmienna jest faktycznie używana gdzie indziej. Ponieważ 5 + 3 jest wyrażeniem, wiemy, że jest użyte tylko w tym jednym wierszu.

Pamiętaj, że działa to tylko w przypadkach, gdy akceptowane jest wyrażenie wartości. W przypadkach, gdy wymagane jest wyrażenie lvalue, musimy mieć obiekt:

#include <iostream>

void addOne(int& value) // pass by non-const references requires lvalue
{
    ++value;
}

int main()
{
    int sum { 5 + 3 };
    addOne(sum);   // okay, sum is an lvalue

    addOne(5 + 3); // compile error: not an lvalue

    return 0;
}

Tymczasowe obiekty klasy

Ten sam problem ma zastosowanie w kontekście typów klas.

Nota autora

Użyjemy tutaj klasy, ale wszystko w tej lekcji, co wykorzystuje inicjalizację list, można w równym stopniu zastosować do struktur inicjowanych przy użyciu inicjalizacji agregowanej.

Poniższy przykład jest podobny do powyższych, ale używa typu klasy zdefiniowanego przez program IntPair zamiast int:

#include <iostream>

class IntPair
{
private:
    int m_x{};
    int m_y{};

public:
    IntPair(int x, int y)
        : m_x { x }, m_y { y }
    {}

    int x() const { return m_x; }
    int y() const { return m_y; }
};

void print(IntPair p)
{
    std::cout << "(" << p.x() << ", " << p.y() << ")\n";        
}
        
int main()
{
    // Case 1: Pass variable
    IntPair p { 3, 4 };
    print(p); // prints (3, 4)
    
    return 0;
}

W przypadku 1 tworzymy instancję zmiennej IntPair p a potem mijanie p do funkcji print().

Jednak p jest używany tylko raz i działa print() zaakceptuje wartości, więc naprawdę nie ma powodu definiować tutaj zmiennej. Pozbądźmy się zatem p.

Możemy to zrobić, przekazując obiekt tymczasowy zamiast nazwanej zmiennej. A obiekt tymczasowy (czasami nazywany obiektem anonimowym lub obiektem bez nazwy) to obiekt, który nie ma nazwy i istnieje tylko przez czas trwania pojedynczego wyrażenia.

Istnieją dwa popularne sposoby tworzenia tymczasowych obiektów typu klasy:

#include <iostream>

class IntPair
{
private:
    int m_x{};
    int m_y{};

public:
    IntPair(int x, int y)
        : m_x { x }, m_y { y }
    {}

    int x() const { return m_x; }
    int y() const{ return m_y; }
};

void print(IntPair p)
{
    std::cout << "(" << p.x() << ", " << p.y() << ")\n";        
}
        
int main()
{
    // Case 1: Pass variable
    IntPair p { 3, 4 };
    print(p);

    // Case 2: Construct temporary IntPair and pass to function
    print(IntPair { 5, 6 } );

    // Case 3: Implicitly convert { 7, 8 } to a temporary Intpair and pass to function
    print( { 7, 8 } );
    
    return 0;
}

W przypadku 2 mówimy kompilatorowi, aby skonstruował plik IntPair obiekt i inicjując go za pomocą { 5, 6 }. Ponieważ ten obiekt nie ma nazwy, jest tymczasowy. Obiekt tymczasowy jest następnie przekazywany do parametru p funkcji print(). Kiedy wywołanie funkcji powróci, obiekt tymczasowy zostanie zniszczony.

W przypadku 3 tworzymy również plik tymczasowy IntPair obiekt do przekazania do funkcji print(). Ponieważ jednak nie określiliśmy jawnie, jaki typ należy skonstruować, kompilator wydedukuje niezbędny typ (IntPair) z parametru funkcji, a następnie niejawnie przekonwertuj { 7, 8 } na IntPair obiektem.

Podsumowując:

IntPair p { 1, 2 }; // create named object p initialized with { 1, 2 }
IntPair { 1, 2 };   // create temporary object initialized with { 1, 2 }
{ 1, 2 };           // compiler will try to convert { 1, 2 } to temporary object matching expected type (typically a parameter or return type)

Ten ostatni przypadek omówimy bardziej szczegółowo na lekcji 14.16 — Konwersja konstruktorów i jawne słowo kluczowe.

Jeszcze kilka przykładów:

std::string { "Hello" }; // create a temporary std::string initialized with "Hello"
std::string {};          // create a temporary std::string using value initialization / default constructor

Tworzenie obiektów tymczasowych poprzez bezpośrednią inicjalizację Opcjonalne

Ponieważ możemy tworzyć obiekty tymczasowe poprzez bezpośrednią inicjalizację listy, możesz się zastanawiać, czy możesz tworzyć obiekty tymczasowe za pomocą innych formularzy inicjalizacji. Nie ma składni umożliwiającej tworzenie obiektów tymczasowych przy użyciu inicjalizacji kopiowania.

Można jednak tworzyć obiekty tymczasowe przy użyciu bezpośredniej inicjalizacji. Na przykład:

Foo (1, 2); //  temporary Foo, direct-initialized with (1, 2) (similar to `Foo { 1, 2 }`)

Pomijając fakt, że na pierwszy rzut oka wygląda to jak wywołanie funkcji, daje to taki sam wynik jak Foo { 1, 2 } (tylko bez zawężania zapobiegania konwersji). Całkiem normalne, prawda?

Pozostałą część tej sekcji poświęcimy teraz na pokazanie, dlaczego prawdopodobnie nie powinieneś tego robić.

Nota autora

Jest to napisane głównie dla przyjemności czytania, a nie jako coś, co musisz przetrawić, zapamiętać i móc wyjaśnić.

Nawet jeśli nie sprawi ci to zbyt wiele przyjemności podczas czytania, może pomóc ci zrozumieć, dlaczego we współczesnym C++ preferowana jest inicjalizacja list!

Spójrzmy teraz na przypadek, w którym nie mamy żadnych argumentów:

Foo();     // temporary Foo, value-initialized (identical to `Foo {}`)

Prawdopodobnie nie spodziewałeś się, że Foo() utworzy tymczasowy inicjalizowany wartością, tak jak robi to Foo {} . A to prawdopodobnie dlatego, że ta składnia ma zupełnie inne znaczenie, gdy jest używana z nazwaną zmienną!

Foo bar{}; // definition of variable bar, value-initialized
Foo bar(); // declaration of function bar that has no parameters and returns a Foo (inconsistent with `Foo bar{}` and `Foo()`)

Gotowy na naprawdę dziwne?!?

Foo(1);    // Function-style cast of literal 1, returns temporary Foo (similar to `Foo { 1 }`)
Foo(bar);  // Defines variable bar of type Foo (inconsistent with `Foo { bar }` and `Foo(1)`)

Czekaj, co?

  • Wersja z literałem 1 w nawiasach zachowuje się spójnie ze wszystkimi innymi wersjami tej składni, które tworzą obiekty tymczasowe.
  • Wersja z identyfikatorem bar w nawiasach definiuje zmienną o nazwie bar (identyczny z Foo bar;). Jeśli bar jest już zdefiniowany, spowoduje to błąd kompilacji redefinicji.

Kompilator wie, że literały nie mogą być używane jako identyfikatory zmiennych, więc może traktować ten przypadek spójnie z innymi.

Na marginesie…

Jeśli zastanawiasz się, dlaczego Foo(bar); zachowuje się identycznie jak Foo bar;

Jedno z najczęstszych zastosowań nawiasów jest grupowanie rzeczy. Na przykład w matematyce: (1 + 2) * 3 daje wynik 9, który jest inny niż 1 + 2 * 3, co daje wynik 7. Jeśli możemy (1 + 2) * 3, nie ma powodu, dla którego nie mielibyśmy tego zrobić (3) * 3.

Z podobnych powodów składnia deklaracji pozwala na grupowanie w nawiasach, a grupy te mogą zawierać tylko jedną rzecz. Foo(bar) jest interpretowana jako definicja zmiennej składająca się z typu Foo po którym następuje grupa składająca się tylko z identyfikatora bar. Dla nas wygląda to po prostu zabawnie, głównie dlatego, że nawiasy nie służą w tym przypadku żadnemu użytecznemu celowi. Nie ma jednak istotnego powodu, aby tego zabronić (ponieważ to tylko znacznie skomplikowałoby składnię języka).

Dla zaawansowanych czytelników

Przyjrzyjmy się nieco bardziej skomplikowanemu przypadkowi. Rozważ stwierdzenie Foo * bar();. Używając (lub nie) nawiasów, możemy całkowicie zmienić znaczenie tej instrukcji:

  • Foo * bar(); (bez dodatkowych nawiasów) domyślnie grupuje * z Foo . Foo* bar(); jest deklaracją funkcji o nazwie bar która nie ma parametrów i zwraca a Foo*.
  • Foo (*bar)(); jawnie grupuje * z bar. Definiuje to wskaźnik funkcji o nazwie bar , który przechowuje adres funkcji, która nie przyjmuje parametrów i zwraca. Foo.
  • Foo (* bar()); jest taki sam jak Foo * bar(); -- nawiasy są w tym przypadku zbędne.

Wreszcie:

  • (Foo *) bar();. Można by się spodziewać, że będzie tak samo jak w przypadku Foo* bar(), ale w rzeczywistości jest to instrukcja wyrażenia wywołująca funkcję bar(), w stylu C rzutuje wartość zwracaną na typ Foo*, a następnie ją odrzuca!

C++ jest czasami bardzo dziwny.

Kluczowa informacja

Nawiasy są złożone, ponieważ są przeciążone i używane w składni bardzo różnych rzeczy. Obejmuje to wywołania funkcji, bezpośrednią inicjalizację obiektów, inicjalizację wartości tymczasowych, rzutowanie w stylu C, grupowanie symboli/identyfikatorów i definicje zmiennych. Kiedy więc w jakiejś składni widzisz nawiasy… nie zawsze jest oczywiste, co otrzymasz!

Z drugiej strony, jeśli widzimy nawiasy klamrowe, wiemy, że mamy do czynienia z obiektami.

OK, koniec zabawy. Wracając do nudnych rzeczy.

Obiekty tymczasowe i zwracanie przez wartość

Gdy funkcja zwraca przez wartość, zwracany obiekt jest obiektem tymczasowym (inicjowanym przy użyciu wartości lub obiektu zidentyfikowanego w instrukcji return).

Oto kilka przykładów:

#include <iostream>

class IntPair
{
private:
    int m_x{};
    int m_y{};

public:
    IntPair(int x, int y)
        : m_x { x }, m_y { y }
    {}

    int x() const { return m_x; }
    int y() const { return m_y; }
};

void print(IntPair p)
{
    std::cout << "(" << p.x() << ", " << p.y() << ")\n";        
}

// Case 1: Create named variable and return
IntPair ret1()
{
    IntPair p { 3, 4 };
    return p; // returns temporary object (initialized using p)
}

// Case 2: Create temporary IntPair and return
IntPair ret2()
{
    return IntPair { 5, 6 }; // returns temporary object (initialized using another temporary object)
}

// Case 3: implicitly convert { 7, 8 } to IntPair and return
IntPair ret3()
{
    return { 7, 8 }; // returns temporary object (initialized using another temporary object)
}
     
int main()
{
    print(ret1());
    print(ret2());
    print(ret3());

    return 0;
}

W przypadku 1, gdy return p tworzony jest i inicjowany obiekt tymczasowy przy użyciu p.

Przypadki w tym przykładzie są analogiczne do przypadki z poprzedniego przykładu.

Kilka uwag

Po pierwsze, podobnie jak w przypadku int, gdy jest używany w wyrażeniu, tymczasowy obiekt klasy jest wartością. Zatem takich obiektów można używać tylko wtedy, gdy akceptowane są wyrażenia wartości.

Po drugie, obiekty tymczasowe tworzone są w momencie definicji i niszczone na końcu pełnego wyrażenia, w którym są zdefiniowane. Pełne wyrażenie to wyrażenie, które nie jest podwyrażeniem.

static_cast w porównaniu z jawną instancją obiektu tymczasowego

W przypadkach, gdy musimy przekonwertować wartość z jednego typu na inny, ale zawężające konwersje nie wchodzą w grę, często mamy opcję użycia static_cast lub jawnej instancji obiektu tymczasowego.

Na przykład:

#include <iostream>

int main()
{
    char c { 'a' };

    std::cout << static_cast<int>( c ) << '\n'; // static_cast returns a temporary int direct-initialized with value of c
    std::cout << int { c } << '\n';             // explicitly creates a temporary int list-initialized with value c

    return 0;
}

static_cast<int>(c) zwraca obiekt tymczasowy int która jest inicjowana bezpośrednio wartością z c. int { c } tworzy tymczasowy int , który jest inicjowany przez listę wartością c. Tak czy inaczej, otrzymamy tymczasowy int zainicjowany wartością c, czyli tego, czego chcemy.

Pokażmy nieco bardziej złożony przykład:

printString.h:

#include <string>
void printString(const std::string &s)
{
    std::cout << s << '\n';
}

main.cpp:

#include "printString.h"
#include <iostream>
#include <string>
#include <string_view>

int main()
{
    std::string_view sv { "Hello" };

    // We want to print sv using the printString() function
    
//    printString(sv); // compile error: a std::string_view won't implicitly convert to a std::string

    printString( static_cast<std::string>(sv) ); // Case 1: static_cast returns a temporary std::string direct-initialized with sv
    printString( std::string { sv } );           // Case 2: explicitly creates a temporary std::string list-initialized with sv
    printString( std::string ( sv ) );           // Case 3: C-style cast returns temporary std::string direct-initialized with sv (avoid this one!)

    return 0;
}

Powiedzmy, że kod w pliku nagłówkowym printString.h nie jest kodem, który możemy modyfikować (np. ponieważ jest dystrybuowany z jakąś biblioteką strony trzeciej używamy i został napisany tak, aby był kompatybilny z C++ 11, który nie obsługuje std::string_view). Jak więc wywołać printString() z sv? Ponieważ a std::string_view nie zostanie w sposób dorozumiany przekonwertowany na a std::string (ze względu na wydajność), nie możemy użyć sv jako argumentu. Musimy użyć jakiejś jawnej formy konwersji.

W przypadku 1 static_cast<std::string>(sv) wywołuje operator static_cast w celu rzutowania sv do std::string. Zwraca to tymczasowy std::string , który został bezpośrednio zainicjowany przy użyciu sv, który jest następnie używany jako argument wywołania funkcji.

W przypadku 2 std::string { sv } tworzy tymczasowy std::string , który jest inicjowany przez listę przy użyciu sv. Ponieważ jest to konstrukcja jawna, konwersja jest dozwolona. Ten plik tymczasowy jest następnie używany jako argument wywołania funkcji.

W przypadku 3, std::string ( sv ) użyj rzutowania w stylu C, aby rzutować sv do std::string. Chociaż w tym przypadku to działa, rzucanie w stylu C może być ogólnie niebezpieczne i należy go unikać. Zwróć uwagę, jak wygląda to podobnie do poprzedniego przypadku!

Najlepsza praktyka

Krótka zasada: Preferuj static_cast przy konwersji na typ podstawowy i tymczasowy inicjowany przez listę podczas konwersji na typ klasowy.

Preferuj static_cast kiedy tworzyć obiekt tymczasowy, gdy spełniony jest którykolwiek z poniższych:

  • Musimy wykonać konwersję zawężającą.
  • Chcemy dokonać to naprawdę oczywiste, że konwertujemy na typ, który spowoduje inne zachowanie (np. a char na int).
  • Z jakiegoś powodu chcemy użyć bezpośredniej inicjalizacji (np. aby uniknąć pierwszeństwa konstruktorów list).

Wolę utworzenie nowego obiektu (przy użyciu inicjalizacji listy), aby utworzyć obiekt tymczasowy, gdy spełniony jest którykolwiek z poniższych:

  • Chcemy użyć inicjalizacji listy (np. dla ochrona przed zawężającymi konwersjami lub dlatego, że musimy wywołać konstruktor list).
  • Musimy podać konstruktorowi dodatkowe argumenty, aby ułatwić konwersję.

Powiązana treść

Konstruktory list omówimy w lekcji 16.2 — Wprowadzenie do std::vector i konstruktorów list.

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