Nota autora
To jest lekcja opcjonalna. Zalecamy krótką lekturę w celu zapoznania się z materiałem, ale do kontynuowania przyszłych lekcji nie jest wymagane wszechstronne zrozumienie.
W lekcji 14.7 — Funkcje składowe zwracające odniesienia do elementów danych, omówiliśmy, w jaki sposób wywoływanie funkcji dostępu, które zwracają referencje do elementów danych, może być niebezpieczne, gdy ukryty obiekt jest wartością. Oto krótkie podsumowanie:
#include <iostream>
#include <string>
#include <string_view>
class Employee
{
private:
std::string m_name{};
public:
Employee(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 { 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() << '\n';
// 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 << '\n'; // undefined behavior
return 0;
}W przypadku 2 obiekt rvalue zwrócony z createEmployee("Garbo") jest niszczony po inicjalizacji ref, pozostawiając ref odniesienie do elementu danych, który właśnie został zniszczony. Późniejsze użycie ref wykazuje niezdefiniowane zachowanie.
To stwarza pewną zagadkę.
- Jeśli nasza
getName()funkcja zwraca wartość, jest to bezpieczne, gdy naszym ukrytym obiektem jest wartość, ale tworzy kosztowną i niepotrzebną kopię, gdy naszym ukrytym obiektem jest wartość (co jest najczęstszym przypadkiem). - Jeśli nasza
getName()funkcja zwraca przez odwołanie do stałej, jest to efektywne (ponieważ nie jest tworzona kopiastd::string), ale może zostać niewłaściwie użyta, gdy ukryty obiekt jest wartością (co skutkuje niezdefiniowanym zachowaniem).
Ponieważ funkcje składowe są zwykle wywoływane dla ukrytych obiektów lvalue, konwencjonalnym wyborem jest zwrócenie przez stałe odwołanie i po prostu unikanie niewłaściwego użycia zwróconego odniesienia w przypadkach, gdy ukrytym obiektem jest rvalue.
Kwalifikatory ref
Podstawą wyzwania przedstawionego powyżej jest to, że chcemy, aby tylko jedna funkcja obsługiwała dwa różne przypadki (jeden, w którym naszym ukrytym obiektem jest lwartość, i drugi, w którym naszym ukrytym obiektem jest rwartość). To, co jest optymalne w jednym przypadku, nie jest idealne w drugim przypadku.
Aby pomóc rozwiązać takie problemy, w C++11 wprowadzono mało znaną funkcję zwaną kwalifikatorem ref , która pozwala nam przeciążać funkcję składową w zależności od tego, czy jest ona wywoływana na ukrytym obiekcie lvalue, czy rvalue. Korzystając z tej funkcji, możemy utworzyć dwie wersje getName() -- jedną dla przypadku, gdy naszym ukrytym obiektem jest wartość, i drugą dla przypadku, gdy naszym ukrytym obiektem jest wartość.
Najpierw zacznijmy od naszej niekwalifikowanej wersji getName()
const std::string& getName() const { return m_name; } // callable with both lvalue and rvalue implicit objectsAby ponownie zakwalifikować tę funkcję, dodajemy kwalifikator & do przeciążenia, które będzie pasować tylko obiekty niejawne lvalue i && kwalifikator do przeciążenia, który będzie pasować tylko do obiektów niejawnych rvalue:
const std::string& getName() const & { return m_name; } // & qualifier overloads function to match only lvalue implicit objects, returns by reference
std::string getName() const && { return m_name; } // && qualifier overloads function to match only rvalue implicit objects, returns by valuePonieważ te funkcje są odrębnymi przeciążeniami, mogą mieć różne typy zwracanych wartości! Nasze przeciążenie kwalifikowane jako lvalue zwracane jest przez stałą referencję, podczas gdy nasze przeciążenie kwalifikowane jako rvalue zwraca przez wartość.
Oto pełny przykład powyższego:
#include <iostream>
#include <string>
#include <string_view>
class Employee
{
private:
std::string m_name{};
public:
Employee(std::string_view name): m_name { name } {}
const std::string& getName() const & { return m_name; } // & qualifier overloads function to match only lvalue implicit objects
std::string getName() const && { return m_name; } // && qualifier overloads function to match only rvalue implicit objects
};
// createEmployee() returns an Employee by value (which means the returned value is an rvalue)
Employee createEmployee(std::string_view name)
{
Employee e { name };
return e;
}
int main()
{
Employee joe { "Joe" };
std::cout << joe.getName() << '\n'; // Joe is an lvalue, so this calls std::string& getName() & (returns a reference)
std::cout << createEmployee("Frank").getName() << '\n'; // Frank is an rvalue, so this calls std::string getName() && (makes a copy)
return 0;
}Pozwala nam to wykonać działanie, gdy naszym niejawnym obiektem jest lwartość, i bezpieczniej, gdy naszym niejawnym obiektem jest wartość.
Dla zaawansowanych czytelników
Powyższe przeciążenie wartości getName() powyżej to potencjalnie nieoptymalne z punktu widzenia wydajności, gdy ukryty obiekt nie jest stałym obiektem tymczasowym. W takich przypadkach dopełnienie ukryte i tak umrze na końcu wyrażenia. Zamiast więc zwracać przez moduł pobierający wartość (prawdopodobnie kosztowną) kopię elementu członkowskiego, możemy go poprosić o próbę przeniesienia elementu członkowskiego (używając std::move).
Można to ułatwić, dodając następujący przeciążony moduł pobierający dla wartości innych niż stałe:
// If the implicit object is a non-const rvalue, use std::move to try to move m_name
std::string getName() && { return std::move(m_name); }Może to albo współistnieć z modułem pobierającym const rvalue, albo możesz go po prostu użyć zamiast tego (ponieważ wartości const są dość niezbyt często).
Omawiamy std::move w lekcji 22.4 — std::move.
Kilka uwag na temat funkcji składowych z kwalifikacją ref
Po pierwsze, w przypadku danej funkcji przeciążenia niekwalifikowane z ref i przeciążenia z kwalifikacją ref nie mogą współistnieć. Użyj jednego lub drugiego.
Po drugie, podobnie jak odwołanie do wartości const może powiązać się z wartością, jeśli tylko jest to wartość. const Funkcja z kwalifikowaną wartością lvalue istnieje, akceptuje ona obiekty ukryte o wartości lvalue lub rvalue.
Po trzecie, każde kwalifikowane przeciążenie można jawnie usunąć (za pomocą = delete), co uniemożliwia wywołania tej funkcji. Na przykład usunięcie wersji z kwalifikacją rvalue uniemożliwia użycie funkcji z ukrytymi obiektami rvalue.
Dlaczego więc nie zalecamy używania kwalifikatorów ref?
Chociaż kwalifikatory ref są fajne, używanie ich w ten sposób ma pewne wady.
- Dodanie przeciążeń rvalue do każdego modułu pobierającego zwracającego referencję powoduje bałagan w klasie, aby zapobiec przypadkom, które mogłyby spowodować, że nie jest tak powszechne i można go łatwo uniknąć, stosując dobre nawyki.
- Zwrócenie przeciążenia wartości przez wartość oznacza, że musimy zapłacić za koszt kopii (lub przeniesienia) nawet w przypadkach, gdy moglibyśmy bezpiecznie użyć referencji (np. w przypadku 1 z przykładu na górze lekcji).
Dodatkowo:
- Większość programistów C++ nie jest świadoma tej funkcji (co może prowadzić do błędów lub nieefektywności w użyciu).
- Biblioteka standardowa zazwyczaj nie korzysta z tej funkcji.
W związku z powyższym nie zalecamy stosowania kwalifikatorów ref jako najlepszej praktyki. Zamiast tego zalecamy, aby zawsze natychmiast używać wyniku funkcji dostępu i nie zapisywać zwróconych referencji do późniejszego wykorzystania.

