W lekcji 12.12 -- Powrót przez referencję i powrót przez adres, omówiliśmy zwrot przez odniesienie. W szczególności zauważyliśmy: „Obiekt zwracany przez referencję musi istnieć po powrocie funkcji”. Oznacza to, że nie powinniśmy zwracać zmiennych lokalnych przez referencję, ponieważ referencja pozostanie nieaktualna po zniszczeniu zmiennej lokalnej. Jednak generalnie dobrze jest zwracać przez referencję albo parametry funkcji przekazane przez referencję, albo zmienne o statycznym czasie trwania (statyczne zmienne lokalne lub zmienne globalne), ponieważ zazwyczaj nie zostaną one zniszczone po zwróceniu funkcji.
Na przykład:
// Pobiera dwa obiekty std::string, zwraca alfabetycznie ten, który jest pierwszy
const std::string& firstAlphabetical(const std::string& a, const std::string& b)
{
return (a < b) ? a : b; // Możemy użyj operatora< na std::string, aby określić, co jest pierwsze w kolejności alfabetycznej
}
int main()
{
std::string hello { "Hello" };
std::string world { "World" };
std::cout << firstAlphabetical(hello, world); // albo hello, albo świat zostanie zwrócony przez odwołanie
return 0;
}Funkcje składowe mogą również zwracać przez referencję i podlegają tym samym zasadom określającym, kiedy można bezpiecznie zwrócić przez referencję, jak funkcje niebędące składowymi. Jednakże funkcje składowe mają jeszcze jeden przypadek, który musimy omówić: funkcje składowe, które zwracają elementy danych przez referencję.
Jest to najczęściej spotykane w przypadku funkcji dostępu pobierających, dlatego zilustrujemy ten temat za pomocą funkcji składowych pobierających. Należy jednak pamiętać, że ten temat dotyczy dowolnej funkcji składowej zwracającej odwołanie do elementu danych.
Zwracanie elementów danych według wartości może być kosztowne
Rozważ następujący przykład:
#include <iostream>
#include <string>
class Employee
{
std::string m_name{};
public:
void setName(std::string_view name) { m_name = name; }
std::string getName() const { return m_name; } // getter zwraca według wartość
};
int main()
{
Employee joe{};
joe.setName("Joe");
std::cout << joe.getName();
return 0;
}W tym przykładzie getName() funkcja dostępu zwraca std::string m_name według wartości.
Chociaż jest to najbezpieczniejsza rzecz, oznacza to również, że kosztowna kopia m_name będzie tworzona co czas getName() wywoływany jest. Ponieważ funkcje dostępu są często wywoływane, generalnie nie jest to najlepszy wybór.
Zwracanie elementów danych przez odniesienie do lwartości
Funkcje składowe mogą również zwracać elementy danych poprzez odniesienie do (const) lwartości.
Elementy danych mają taki sam czas życia jak obiekt je zawierający. Ponieważ funkcje składowe są zawsze wywoływane na obiekcie, a obiekt ten musi istnieć w zasięgu obiektu wywołującego, ogólnie rzecz biorąc, funkcja składowa może bezpiecznie zwrócić element danych za pomocą odniesienia do (const) lwartości (ponieważ element członkowski zwracany przez referencję nadal będzie istniał w zakresie obiektu wywołującego, gdy funkcja powróci).
Zaktualizujmy powyższy przykład tak, aby getName() zwraca m_name przez odwołanie do wartości const:
#include <iostream>
#include <string>
class Employee
{
std::string m_name{};
public:
void setName(std::string_view name) { m_name = name; }
const std::string& getName() const { return m_name; } // getter zwraca według stałej referencji
};
int main()
{
Employee joe{}; // joe istnieje do końca funkcji
joe.setName("Joe");
std::cout << joe.getName(); // uwaga joe.m_name przez odniesienie
return 0;
}Teraz gdy joe.getName() zostanie wywołane, joe.m_name jest zwracane przez odniesienie do osoby wywołującej, co pozwala uniknąć konieczności wykonywania kopii. Następnie wywołujący używa tego odniesienia do print joe.m_name do konsoli.
Ponieważ joe istniejącego w zakresie wywołującego aż do końca main() funkcji, odwołanie do joe.m_name jest również ważne przez ten sam czas trwania.
Kluczowa informacja
Zwrócenie (stałej) wartości odniesienia do elementu danych jest w porządku. Niejawny obiekt (zawierający element danych) nadal istnieje w zasięgu obiektu wywołującego po powrocie funkcji, więc wszelkie zwrócone referencje będą prawidłowe.
Typ zwracany przez funkcję składową zwracającą odwołanie do elementu danych powinien odpowiadać typowi elementu danych.
Ogólnie rzecz biorąc, typ zwracany przez funkcję składową zwracaną przez referencję powinien odpowiadać typowi zwracanego elementu danych. W powyższym przykładzie m_name jest typu std::string, więc getName() zwraca const std::string&.
Zwrócenie std::string_view wymagałoby utworzenia tymczasowego std::string_view i zwrócenia go przy każdym wywołaniu funkcji. To niepotrzebnie nieefektywne. Jeśli wywołujący chce std::string_view, może sam dokonać konwersji.
Najlepsza praktyka
Funkcja członkowska zwracająca referencję powinna zwrócić referencję tego samego typu, co zwracany element danych, aby uniknąć niepotrzebnych konwersji.
W przypadku getterów użycie auto , aby kompilator wydedukował typ zwracany na podstawie zwracanego elementu członkowskiego, jest użytecznym sposobem zapewnienia, że nie nastąpi żadna konwersja wystąpić:
#include <iostream>
#include <string>
class Employee
{
std::string m_name{};
public:
void setName(std::string_view name) { m_name = name; }
const auto& getName() const { return m_name; } // używa `auto` do wydedukowania typu zwracanego z m_name
};
int main()
{
Employee joe{}; // joe istnieje do końca funkcji
joe.setName("Joe");
std::cout << joe.getName(); // uwaga joe.m_name przez odniesienie
return 0;
}Powiązana treść
Omawiamy auto typy zwracane w lekcji 10.9 -- Dedukcja typu dla funkcji.
Jednak użycie auto typu zwracanego przesłania typ zwracany przez moduł pobierający z punktu widzenia dokumentacji. Na przykład:
const auto& getName() const { return m_name; } // używa `auto` do wydedukowania typu zwracanego z m_nameNie jest jasne, jaki rodzaj łańcucha faktycznie zwraca ta funkcja (może to być std::string, std::string_view, C-style string lub coś zupełnie innego!).
Z tego powodu generalnie preferujemy jawne typy zwracanych wartości.
Rwartościuj obiekty ukryte i zwracaj przez referencję
Jest jeden przypadek, w którym musimy zachować trochę ostrożności. W powyższym przykładzie joe jest obiektem wartościowym, który istnieje do końca funkcji. Dlatego referencja zwrócona przez joe.getName() będzie również ważna do końca funkcji.
Ale co, jeśli naszym ukrytym obiektem jest zamiast tego wartość (taka jak wartość zwracana przez jakąś funkcję, która zwraca wartość)? Obiekty Rvalue są niszczone na końcu pełnego wyrażenia, w którym zostały utworzone. Kiedy obiekt rvalue zostanie zniszczony, wszelkie odniesienia do elementów tego obiektu rvalue zostaną unieważnione i pozostawione w zawieszeniu, a użycie takich odniesień spowoduje niezdefiniowane zachowanie.
Dlatego też odwołanie do elementu członkowskiego obiektu rvalue może być bezpiecznie użyte tylko w pełnym wyrażeniu, w którym obiekt rvalue został utworzony.
Wskazówka
Co to jest pełne wyrażenie, omówiliśmy w lekcji 1.10 -- Wprowadzenie do wyrażeń.
Ostrzeżenie
Obiekt rvalue jest niszczony na końcu pełnego wyrażenia w jakim jest stworzony. Wszelkie odniesienia do elementów obiektu rvalue pozostają w tym momencie nieaktualne.
Odwołanie do elementu obiektu rvalue może być bezpiecznie użyte tylko w pełnym wyrażeniu, w którym tworzony jest obiekt rvalue.
Przyjrzyjmy się kilku powiązanym z tym przypadkom:
#include <iostream>
#include <string>
#include <string_view>
class Employee
{
std::string m_name{};
public:
void setName(std::string_view name) { m_name = name; }
const std::string& getName() const { return m_name; } // getter zwraca według stałej referencji
};
// createEmployee() zwraca pracownika według wartości (co oznacza, że zwrócona wartość jest wartością)
Employee createEmployee(std::string_view name)
{
Employee e;
e.setName(name);
return e;
}
int main()
{
// Przypadek 1: OK: użyj zwróconej referencji do członka obiektu klasy rvalue w tym samym wyrażeniu
std::cout << createEmployee("Frank").getName();
// Przypadek 2: źle: zapisz zwrócone odwołanie do członka obiektu klasy rvalue do późniejszego wykorzystania
const std::string& ref { createEmployee("Garbo").getName() }; // referencja zawiesza się, gdy wartość zwracana przez createEmployee() zostaje zniszczona
std::cout << ref; // undefined behavior
// Przypadek 3: OK: skopiuj wartość odniesienia do zmiennej lokalnej do późniejszego wykorzystania
std::string val { createEmployee("Hans").getName() }; // tworzy kopię elementu, do którego się odwołuje
std::cout << val; // OK: val jest niezależna od elementu, do którego się odwołuje
return 0;
}Po wywołaniu createEmployee() jest wywoływane, zwraca Employee obiekt według wartości. Ten zwrócony obiekt Employee jest wartością, która będzie istnieć do końca pełnego wyrażenia zawierającego wywołanie createEmployee(). Kiedy obiekt wartości zostanie zniszczony, wszelkie odniesienia do elementów tego obiektu staną się nieaktualne.
W przypadku 1 wywołujemy obiekt createEmployee("Frank"), który zwraca obiekt wartości Employee . Następnie wywołujemy getName() ten obiekt wartości, który zwraca referencję do m_name. To odwołanie jest następnie natychmiast wykorzystywane do wydrukowania nazwy na konsoli. W tym momencie pełne wyrażenie zawierające wywołanie createEmployee("Frank") kończy się, a obiekt rvalue i jego elementy zostają zniszczone. Ponieważ ani obiekt rvalue, ani jego elementy nie są używane poza tym punktem, ten przypadek jest w porządku.
W przypadku 2 napotykamy problemy. Najpierw createEmployee("Garbo") zwraca obiekt wartości. Następnie wywołujemy getName() , aby uzyskać odwołanie do m_name elementu tej wartości. Ten m_name element jest następnie używany do inicjalizacji ref. W tym momencie pełne wyrażenie zawierające wywołanie createEmployee("Garbo") kończy się, a obiekt rvalue i jego elementy zostają zniszczone. To pozostawia ref wisające. Zatem, gdy używamy ref w kolejnej instrukcji, uzyskujemy dostęp do wiszącego odniesienia i niezdefiniowanych wyników zachowania.
Kluczowa informacja
Ocena pełnego wyrażenia kończy po jakiekolwiek użycie tego pełnego wyrażenia jako inicjatora. Pozwala to na inicjowanie obiektów wartością tego samego typu (ponieważ wartość nie zostanie zniszczona do czasu inicjalizacji).
Ale co, jeśli chcemy zapisać wartość z funkcji, która zwraca element członkowski przez odwołanie do późniejszego wykorzystania? Zamiast używać zwróconego odniesienia do inicjalizacji lokalnej zmiennej odniesienia, możemy zamiast tego użyć zwróconego odniesienia do inicjalizacji zmiennej lokalnej niebędącej referencją.
W przypadku 3 używamy zwróconego odniesienia do inicjalizacji zmiennej lokalnej niebędącej referencją val. Spowoduje to skopiowanie pręta, do którego się odwołujemy, do val. Po inicjalizacji val istnieje niezależnie od odniesienia. Zatem kiedy obiekt rvalue zostanie następnie zniszczony, val nie ma to na to wpływu. Zatem val można bez problemu wyprowadzić w przyszłych instrukcjach.
Korzystanie z funkcji składowych, które bezpiecznie zwracają przez referencję
Pomimo potencjalnego zagrożenia związanego z obiektami ukrytymi o wartości rvalue, metody pobierające zwykle zwracają typy, których kopiowanie jest kosztowne, poprzez odwołanie do stałej, a nie przez wartość.
Biorąc to pod uwagę, porozmawiajmy o tym, jak możemy bezpiecznie wykorzystać wartości zwracane przez takie funkcje. Trzy przypadki w powyższym przykładzie ilustrują trzy kluczowe punkty:
- Wolę używać wartości zwracanej przez funkcję składową, która natychmiast zwraca przez referencję (zilustrowane w przypadku 1). Ponieważ działa to zarówno z obiektami lvalue, jak i rvalue, jeśli zawsze będziesz to robić, unikniesz problemów.
- Nie „zapisuj” zwróconej referencji do późniejszego wykorzystania (jak pokazano w przypadku 2), chyba że jesteś pewien, że ukryty obiekt jest wartością. Jeśli zrobisz to z ukrytym obiektem rvalue, podczas korzystania z zawieszonego odniesienia spowoduje to niezdefiniowane zachowanie.
- Jeśli musisz utrwalić zwrócone odwołanie do późniejszego wykorzystania i nie masz pewności, czy ukryty obiekt jest wartością, użyj zwróconego odniesienia jako inicjatora dla zmiennej lokalnej niebędącej referencją, co spowoduje utworzenie kopii elementu członkowskiego, do którego odwołuje się odwołanie, do zmiennej lokalnej (zilustrowane w przypadku 3).
Najlepsza praktyka
Wolę użyć zwrotu wartość funkcji składowej, która natychmiast zwraca przez referencję, aby uniknąć problemów z zawieszonymi referencjami, gdy ukryty obiekt jest wartością.
Nie zwracaj odwołań innych niż stałe do prywatnych elementów danych
Ponieważ odwołanie działa tak samo jak obiekt, do którego się odwołuje, funkcja członkowska, która zwraca odwołanie inne niż stałe, zapewnia bezpośredni dostęp do tego elementu członkowskiego (nawet jeśli element jest prywatny).
Na przykład:
#include <iostream>
class Foo
{
private:
int m_value{ 4 }; // private member
public:
int& value() { return m_value; } // zwraca odwołanie inne niż stałe (nie rób tego)
};
int main()
{
Foo f{}; // f.m_wartość jest inicjowana do wartości domyślnej 4
f.value() = 5; // Odpowiednik m_value = 5
std::cout << f.value(); // prints 5
return 0;
}Ponieważ value() zwraca odwołanie inne niż stałe do m_value, osoba wywołująca może użyć tego odniesienia, aby uzyskać bezpośredni dostęp (i zmienić wartość) m_value.
Pozwala to wywołującemu obalić system kontroli dostępu.
Funkcje składowe Const nie mogą zwracać odwołań innych niż stałe do elementów danych
Funkcja członkowska const nie może zwracać do elementów referencji innych niż stałe. Ma to sens — funkcja składowa const nie może modyfikować stanu obiektu ani nie może wywoływać funkcji, które modyfikowałyby stan obiektu. Nie powinna robić niczego, co mogłoby prowadzić do modyfikacji obiektu.
Gdyby funkcja składowa const mogła zwracać do elementu odwołanie inne niż stałe, udostępniałaby osobie wywołującej możliwość bezpośredniej modyfikacji tego elementu. Narusza to intencję funkcji składowej const.

