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:
// 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); // either hello or world will be returned by reference
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 returns by value
};
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 returns by const reference
};
int main()
{
Employee joe{}; // joe exists until end of function
joe.setName("Joe");
std::cout << joe.getName(); // returns joe.m_name by reference
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; } // uses `auto` to deduce return type from m_name
};
int main()
{
Employee joe{}; // joe exists until end of function
joe.setName("Joe");
std::cout << joe.getName(); // returns joe.m_name by reference
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; } // uses `auto` to deduce return type from 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 returns by const reference
};
// createEmployee() returns an Employee by value (which means the returned value is an rvalue)
Employee createEmployee(std::string_view name)
{
Employee e;
e.setName(name);
return e;
}
int main()
{
// Case 1: okay: use returned reference to member of rvalue class object in same expression
std::cout << createEmployee("Frank").getName();
// Case 2: bad: save returned reference to member of rvalue class object for use later
const std::string& ref { createEmployee("Garbo").getName() }; // reference becomes dangling when return value of createEmployee() is destroyed
std::cout << ref; // undefined behavior
// Case 3: okay: copy referenced value to local variable for use later
std::string val { createEmployee("Hans").getName() }; // makes copy of referenced member
std::cout << val; // okay: val is independent of referenced member
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; } // returns a non-const reference (don't do this)
};
int main()
{
Foo f{}; // f.m_value is initialized to default value 4
f.value() = 5; // The equivalent of 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.

