14.7 — Funkcje członkowskie zwracające odniesienia do elementów danych

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_name

Nie 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.

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