12.12 — Powrót przez odniesienie i powrót przez adres

W poprzednich lekcjach omawialiśmy, że podczas przekazywania argumentu przez wartość kopia argumentu jest tworzona do parametru funkcji. W przypadku typów podstawowych (które są tanie w kopiowaniu) jest to w porządku. Jednak kopiowanie jest zazwyczaj kosztowne w przypadku typów klas (takich jak std::string). Możemy uniknąć kosztownej kopii, wykorzystując zamiast tego przekazywanie (const) referencji (lub przekazywanie adresu).

Podobną sytuację napotykamy podczas zwracania przez wartość: kopia zwracanej wartości jest przekazywana z powrotem do osoby wywołującej. Jeśli typem zwracanym funkcji jest typ klasowy, może to być kosztowne.

std::string returnByValue(); // returns a copy of a std::string (expensive)

Return by reference

W przypadkach, gdy przekazujemy typ klasy z powrotem do wywołującego, możemy (ale nie musimy) chcieć zwrócić zamiast tego przez referencję. Return by reference zwraca referencję powiązaną ze zwracanym obiektem, co pozwala uniknąć tworzenia kopii zwracanej wartości. Aby zwrócić przez referencję, po prostu definiujemy wartość zwracaną funkcji jako typ referencyjny:

std::string&       returnByReference(); // returns a reference to an existing std::string (cheap)
const std::string& returnByReferenceToConst(); // returns a const reference to an existing std::string (cheap)

Oto program akademicki demonstrujący mechanikę powrotu przez referencję:

#include <iostream>
#include <string>

const std::string& getProgramName() // returns a const reference
{
    static const std::string s_programName { "Calculator" }; // has static duration, destroyed at end of program

    return s_programName;
}

int main()
{
    std::cout << "This program is named " << getProgramName();

    return 0;
}

Ten program wypisuje:

This program is named Calculator

Ponieważ getProgramName() zwraca stałą referencję, gdy linia return s_programName jest wykonywana, getProgramName() zwróci stałą referencję do s_programName (w ten sposób unikając tworzenia kopii). To odwołanie do stałej może następnie zostać wykorzystane przez osobę wywołującą w celu uzyskania dostępu do wartości s_programName, która jest drukowana.

Obiekt zwracany przez referencję musi istnieć po zwróceniu przez funkcję

Korzystanie z funkcji return przez referencję wiąże się z jednym poważnym zastrzeżeniem: programista muszą upewnia się, że obiekt, do którego się odwołuje, przetrwa działanie funkcji zwracającej referencję. W przeciwnym razie zwracane odniesienie pozostanie zawieszone (odwołuje się do obiektu, który został zniszczony), a użycie tego odniesienia spowoduje niezdefiniowane zachowanie.

W powyższym programie, ponieważ s_programName ma statyczny czas trwania, s_programName będzie istniał do końca programu. Kiedy main() uzyskuje dostęp do zwróconej referencji, tak naprawdę uzyskuje dostęp do s_programName, co jest w porządku, ponieważ s_programName nie zostanie zniszczony dopiero później.

Teraz zmodyfikujmy powyższy program, aby pokazać, co się stanie w przypadku, gdy nasza funkcja zwróci wiszące odwołanie:

#include <iostream>
#include <string>

const std::string& getProgramName()
{
    const std::string programName { "Calculator" }; // now a non-static local variable, destroyed when function ends

    return programName;
}

int main()
{
    std::cout << "This program is named " << getProgramName(); // undefined behavior

    return 0;
}

Wynik tego programu jest niezdefiniowany. Kiedy getProgramName() powraca, zwracane jest odwołanie powiązane ze zmienną lokalną programName . Następnie, ponieważ programName jest zmienną lokalną o automatycznym czasie trwania, programName jest niszczona na końcu funkcji. Oznacza to, że zwrócona referencja jest teraz zawieszona, a użycie programName w klasie main() funkcji skutkuje niezdefiniowanym zachowaniem.

Nowoczesne kompilatory zwrócą ostrzeżenie lub błąd, jeśli spróbujesz zwrócić zmienną lokalną przez referencję (więc powyższy program może nawet się nie skompilować), ale kompilatory czasami mają problemy z wykryciem bardziej skomplikowanych przypadków.

Ostrzeżenie

Obiekty zwrócone przez referencję muszą wykraczać poza zakres funkcji zwracającej referencję, w przeciwnym razie spowoduje to zawieszanie się referencji. Nigdy nie zwracaj (niestatycznej) zmiennej lokalnej ani tymczasowej przez odniesienie.

Wydłużanie czasu życia nie działa poza granicami funkcji

Przyjrzyjmy się przykładowi, w którym zwracamy tymczasową zmienną przez odniesienie:

#include <iostream>

const int& returnByConstReference()
{
    return 5; // returns const reference to temporary object
}

int main()
{
    const int& ref { returnByConstReference() };

    std::cout << ref; // undefined behavior

    return 0;
}

W powyższym programie returnByConstReference() zwraca literał całkowity, ale zwracany typ funkcji to const int&. Powoduje to utworzenie i zwrócenie tymczasowego odniesienia powiązanego z tymczasowym obiektem posiadającym wartość 5. To zwrócone odwołanie jest kopiowane do tymczasowego odniesienia w zakresie obiektu wywołującego. Obiekt tymczasowy następnie wychodzi poza zakres, pozostawiając tymczasowe odniesienie w zakresie obiektu wywołującego zawieszone.

Do czasu, gdy tymczasowe odniesienie w zakresie obiektu wywołującego zostanie powiązane ze stałą zmienną referencyjną ref (w main()), jest już za późno na przedłużenie czasu życia obiektu tymczasowego - ponieważ został on już zniszczony. Zatem ref to odwołanie wiszące i użycie wartości ref spowoduje niezdefiniowane zachowanie.

Oto mniej oczywisty przykład, który podobnie nie działa:

#include <iostream>

const int& returnByConstReference(const int& ref)
{
    return ref;
}

int main()
{
    // case 1: direct binding
    const int& ref1 { 5 }; // extends lifetime
    std::cout << ref1 << '\n'; // okay

    // case 2: indirect binding
    const int& ref2 { returnByConstReference(5) }; // binds to dangling reference
    std::cout << ref2 << '\n'; // undefined behavior

    return 0;
}

W przypadku 2 tworzony jest obiekt tymczasowy do przechowywania wartości 5, z którą wiąże się parametr funkcji ref . Funkcja po prostu zwraca to odwołanie z powrotem do wywołującego, który następnie używa tego odniesienia do inicjalizacji ref2. Ponieważ nie jest to bezpośrednie powiązanie z obiektem tymczasowym (ponieważ odwołanie zostało odesłane przez funkcję), przedłużenie okresu istnienia nie ma zastosowania. Pozostawia to ref2 zawieszone, a jego późniejsze użycie jest niezdefiniowanym zachowaniem.

Ostrzeżenie

Wydłużanie czasu istnienia referencji nie działa ponad granicami funkcji.

Nie zwracaj innych niż stałe statycznych zmiennych lokalnych przez referencję

W oryginalnym przykładzie powyżej zwróciliśmy stałą statyczną zmienną lokalną przez referencję, aby w prosty sposób zilustrować mechanikę zwrotu przez referencję. Jednakże zwracanie niestałych statycznych zmiennych lokalnych przez odwołanie jest dość nieidiomatyczne i generalnie należy go unikać. Oto uproszczony przykład ilustrujący jeden z takich problemów, który może wystąpić:

#include <iostream>
#include <string>

const int& getNextId()
{
    static int s_x{ 0 }; // note: variable is non-const
    ++s_x; // generate the next id
    return s_x; // and return a reference to it
}

int main()
{
    const int& id1 { getNextId() }; // id1 is a reference
    const int& id2 { getNextId() }; // id2 is a reference

    std::cout << id1 << id2 << '\n';

    return 0;
}

Ten program wypisuje:

22

Dzieje się tak, ponieważ id1 i id2 odwołują się do tego samego obiektu (zmienna statyczna s_x), więc gdy cokolwiek (np. getNextId()) modyfikuje tę wartość, wszystkie odniesienia uzyskują teraz dostęp do zmodyfikowanej wartości.

Powyższy przykład można naprawić, tworząc id1 i id2 normalne zmienne (a nie odniesienia) więc zapisują kopię zwracanej wartości zamiast odniesienia do s_x.

Dla zaawansowanych czytelników

Oto kolejny przykład z mniej oczywistą wersją tego samego problemu:

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

std::string& getName()
{
    static std::string s_name{};
    std::cout << "Enter a name: ";
    std::cin >> s_name;
    return s_name;
}

void printFirstAlphabetical(const std::string& s1, const std::string& s2)
{
    if (s1 < s2)
        std::cout << s1 << " comes before " << s2 << '\n';
    else
        std::cout << s2 << " comes before " << s1 << '\n';
}

int main()
{
    printFirstAlphabetical(getName(), getName());
    
    return 0;
}

Oto wynik jednego uruchomienia tego programu:

Enter a name: Dave
Enter a name: Stan
Stan comes before Stan

W tym przykładzie getName() zwraca odniesienie do statycznego lokalnego s_name. Zainicjowanie const std::string& odniesieniem do s_name powoduje, że std::string& powiąże się z s_name (nie tworzy kopii).

W ten sposób oba s1 i s2 kończą przeglądanie s_name (któremu przypisano wprowadzone przez nas nazwisko).

Zauważ, że jeśli zamiast tego użyj std::string_view parametrów, pierwszy std::string_view parametr zostanie unieważniony, gdy std::string podstawowy zostanie zmieniony.

Innym problemem, który często występuje w programach, które zwracają niestały statyczny lokalny przez odniesienie, jest to, że nie ma ustandaryzowanego sposobu resetowania s_x powrotu do stanu domyślnego. Takie programy muszą albo używać niekonwencjonalnych rozwiązań (np. parametru funkcji resetowania), albo można je zresetować jedynie poprzez zamknięcie i ponowne uruchomienie programu.

Najlepsza praktyka

Unikaj zwracania odniesień do niestałych lokalnych zmiennych statycznych.

Zwracanie stałego odniesienia do const lokalnej zmiennej statycznej jest czasami wykonywane, jeśli utworzenie i/lub inicjalizacja zmiennej lokalnej zwracanej przez referencję jest kosztowne (więc nie trzeba odtwarzać zmiennej przy każdym wywołaniu funkcji). Jest to jednak rzadkie.

Zwracanie stałego odniesienia do const zmiennej globalnej jest czasami wykonywane również w celu hermetyzacji dostępu do zmiennej globalnej. Omawiamy to na lekcji 7.8 — Dlaczego (nie stałe) zmienne globalne są złe. Jeśli jest to używane celowo i ostrożnie, jest to również w porządku.

Przypisanie/inicjowanie normalnej zmiennej ze zwróconym odniesieniem powoduje, że kopia

Jeśli funkcja zwróci referencję, a referencja ta zostanie użyta do inicjalizacji lub przypisania zmiennej niebędącej referencją, zwrócona wartość zostanie skopiowana (tak jakby została zwrócona przez wartość).

#include <iostream>
#include <string>

const int& getNextId()
{
    static int s_x{ 0 };
    ++s_x;
    return s_x;
}

int main()
{
    const int id1 { getNextId() }; // id1 is a normal variable now and receives a copy of the value returned by reference from getNextId()
    const int id2 { getNextId() }; // id2 is a normal variable now and receives a copy of the value returned by reference from getNextId()

    std::cout << id1 << id2 << '\n';

    return 0;
}

W powyższym przykładzie getNextId() zwraca odniesienie, ale id1 i id2 są zmiennymi niebędącymi odniesieniem. W takim przypadku wartość zwróconej referencji jest kopiowana do normalnej zmiennej. Zatem ten program wypisuje:

12

Zauważ także, że jeśli program zwróci zawieszone odniesienie, odniesienie to pozostanie nieaktualne przed wykonaniem kopii, co doprowadzi do niezdefiniowanego zachowania:

#include <iostream>
#include <string>

const std::string& getProgramName() // will return a const reference
{
    const std::string programName{ "Calculator" };

    return programName;
}

int main()
{
    std::string name { getProgramName() }; // makes a copy of a dangling reference
    std::cout << "This program is named " << name << '\n'; // undefined behavior

    return 0;
}

Można zwrócić parametry odniesienia przez odniesienie

Jest sporo przypadków, w których zwracanie obiektów przez odniesienie ma sens i z wieloma z nich spotkamy się w przyszłych lekcjach. Istnieje jednak jeden przydatny przykład, który możemy teraz pokazać.

Jeśli parametr jest przekazywany do funkcji przez referencję, można bezpiecznie zwrócić ten parametr przez referencję. Ma to sens: aby przekazać argument do funkcji, argument musi istnieć w zasięgu wywołującego. Kiedy wywoływana funkcja powraca, obiekt ten musi nadal istnieć w zasięgu wywołującego.

Oto prosty przykład takiej funkcji:

#include <iostream>
#include <string>

// Takes two std::string objects, returns the one that comes first alphabetically
const std::string& firstAlphabetical(const std::string& a, const std::string& b)
{
	return (a < b) ? a : b; // We can use operator< on std::string to determine which comes first alphabetically
}

int main()
{
	std::string hello { "Hello" };
	std::string world { "World" };

	std::cout << firstAlphabetical(hello, world) << '\n';

	return 0;
}

Wypisuje:

Hello

W powyższej funkcji wywołujący przekazuje dwa obiekty std::string przez odwołanie do stałej, a którykolwiek z tych ciągów znaków pojawi się pierwszy w kolejności alfabetycznej, jest przekazywany z powrotem przez odwołanie do stałej. Gdybyśmy użyli przekazywania przez wartość i zwracania przez wartość, utworzylibyśmy do 3 kopii std::string (po jednej dla każdego parametru i jednej dla zwracanej wartości). Używając przekazywania przez referencję/powrotu przez referencję, możemy uniknąć tych kopii.

Nie ma nic złego w tym, że rwartość przekazana przez odwołanie const zostanie zwrócona przez odwołanie const

Gdy argument parametru referencji const jest wartością, nadal można zwrócić ten parametr poprzez odwołanie do const.

Dzieje się tak, ponieważ rwartości nie są niszczone aż do końca pełnego wyrażenia, w którym są utworzony.

Najpierw spójrzmy na ten przykład:

#include <iostream>
#include <string>

std::string getHello()
{
    return "Hello"; // implicit conversion to std::string
}

int main()
{
    const std::string s{ getHello() };

    std::cout << s;
    
    return 0;
}

W tym przypadku getHello() zwraca a std::string według wartości, która jest wartością. Ta wartość jest następnie używana do inicjalizacji s. Po inicjalizacji s wyrażenie, w którym utworzono rwartość, zostało zakończone, a rwartość zostaje zniszczona.

Przyjrzyjmy się teraz temu podobnemu przykładowi:

#include <iostream>
#include <string>

const std::string& foo(const std::string& s)
{
    return s;
}

std::string getHello()
{
    return "Hello"; // implicit conversion to std::string
}

int main()
{
    const std::string s{ foo(getHello()) };

    std::cout << s;
    
    return 0;
}

Jedyna różnica w tym przypadku polega na tym, że rwartość jest przekazywana przez stałą referencję do foo() , a następnie zwracana przez stałą referencję z powrotem do obiektu wywołującego, zanim zostanie użyta zainicjuj s. Wszystko inne działa identycznie.

Podobny przypadek omawiamy na lekcji 14.6 — Funkcje dostępu.

Osoba wywołująca może modyfikować wartości poprzez referencję

Gdy argument jest przekazywany do funkcji przez odwołanie inne niż stałe, funkcja może użyć referencji do zmodyfikowania wartości argumentu.

Podobnie, gdy z funkcji zostanie zwrócone odwołanie inne niż stałe, osoba wywołująca może użyć referencji do zmodyfikowania wartości zwracany.

Oto ilustrujący przykład:

#include <iostream>

// takes two integers by non-const reference, and returns the greater by reference
int& max(int& x, int& y)
{
    return (x > y) ? x : y;
}

int main()
{
    int a{ 5 };
    int b{ 6 };

    max(a, b) = 7; // sets the greater of a or b to 7

    std::cout << a << b << '\n';
        
    return 0;
}

W powyższym programie max(a, b) wywołuje funkcję max() z a i b jako argumentami. Parametr referencyjny x wiąże się z argumentem a, a parametr referencyjny y wiąże się z argumentem b. Funkcja następnie określa, który z x (5) i y (6) jest większy. W tym przypadku jest to y, zatem funkcja zwraca y (która nadal jest powiązana z b) z powrotem do obiektu wywołującego. Następnie obiekt wywołujący przypisuje wartość 7 do zwróconego odniesienia.

Dlatego wyrażenie max(a, b) = 7 w rzeczywistości przekształca się w b = 7.

Wypisuje:

57

Zwrot przez adres

Zwrot przez adres , co działa prawie identycznie, jak w przypadku zwracania przez referencję, z tą różnicą, że zamiast referencji do obiektu zwracany jest wskaźnik do obiektu. Powrót przez adres ma to samo podstawowe zastrzeżenie co powrót przez referencję — obiekt zwracany przez adres musi przekraczać zakres funkcji zwracającej adres, w przeciwnym razie osoba wywołująca otrzyma zawieszony wskaźnik.

Główną zaletą zwrotu przez adres w porównaniu ze zwrotem przez referencję jest to, że funkcja return nullptr nie ma prawidłowego obiektu do zwrócenia. Załóżmy na przykład, że mamy listę uczniów, których chcemy przeszukać. Jeśli znajdziemy na liście ucznia, którego szukamy, możemy zwrócić wskaźnik do obiektu reprezentującego pasującego ucznia. Jeśli nie znajdziemy żadnego pasującego ucznia, możemy powrócić nullptr w celu wskazania, że ​​nie znaleziono pasującego obiektu ucznia.

Główną wadą zwrotu według adresu jest to, że osoba dzwoniąca musi pamiętać o wykonaniu nullptr sprawdź przed wyłuskaniem zwracanej wartości, w przeciwnym razie może nastąpić wyłuskanie wskaźnika zerowego i w rezultacie nastąpi niezdefiniowane zachowanie. Ze względu na to niebezpieczeństwo należy preferować zwrot przez referencję zamiast zwrotu po adresie, chyba że wymagana jest możliwość zwrócenia „żadnego obiektu”.

Najlepsza praktyka

Preferuj zwrot przez referencję zamiast zwrotu po adresie, chyba że możliwość zwrócenia „żadnego obiektu” (za pomocą nullptr) jest ważna.

Powiązana treść

Jeśli potrzebujesz możliwości zwrócenia „żadnego obiektu” lub wartości (a nie obiektu), 12.15 -- std::opcjonalne opisuje dobro alternatywa.

Powiązana treść

Zobacz 5.9 — std::string_view (część 2) aby uzyskać krótką instrukcję, kiedy zwrócić std::string_view vs const std::string& .

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