Jak działa gwarantowana eliminacja kopii?

Odpowiedzi:

129

Kopiowanie elizji było dozwolone w wielu okolicznościach. Jednak nawet jeśli było to dozwolone, kod nadal musiał działać tak, jakby kopia nie została usunięta. Mianowicie musiał istnieć dostępny konstruktor kopiowania i / lub przenoszenia.

Gwarantowana kopia elizja redefiniuje numer C ++ pojęć, takich, że pewne okoliczności, w których może być pomijana kopii / porusza się w rzeczywistości nie sprowokować Kopiuj / przenieś w ogóle . Kompilator nie usuwa kopii; norma mówi, że takie kopiowanie nigdy się nie wydarzy.

Rozważ tę funkcję:

T Func() {return T();}

Zgodnie z niegwarantowanymi regułami usuwania kopii, spowoduje to utworzenie tymczasowego, a następnie przeniesienie z tego tymczasowego do wartości zwracanej funkcji. Ta operacja przenoszenia może zostać usunięta, ale Tnadal musi mieć dostępny konstruktor przenoszenia, nawet jeśli nigdy nie jest używany.

Podobnie:

T t = Func();

To jest inicjalizacja kopii t. Spowoduje to skopiowanie inicjalizacji tz wartością zwracaną Func. Jednak Tnadal musi mieć konstruktora przenoszenia, mimo że nie zostanie wywołany.

Gwarantowana eliminacja kopii na nowo definiuje znaczenie wyrażenia prvalue . Przed C ++ 17 prvalues ​​są obiektami tymczasowymi. W C ++ 17 wyrażenie prvalue jest po prostu czymś, co może zmaterializować się tymczasowo, ale nie jest jeszcze tymczasowe.

Jeśli użyjesz prvalue do zainicjowania obiektu typu prvalue, wtedy żadna tymczasowa nie zostanie zmaterializowana. Kiedy to zrobisz return T();, inicjalizuje wartość zwracaną funkcji przez prvalue. Ponieważ ta funkcja zwraca T, nie jest tworzony obiekt tymczasowy; inicjalizacja wartości pr po prostu bezpośrednio inicjalizuje wartość zwracaną.

Należy zrozumieć, że skoro zwracana wartość jest wartością prvalue, nie jest jeszcze obiektem . Jest to jedynie inicjalizacja obiektu, tak jak T()jest.

Gdy to zrobisz T t = Func();, prvalue wartości zwracanej bezpośrednio inicjalizuje obiekt t; nie ma etapu „utwórz tymczasowy i skopiuj / przenieś”. Ponieważ Func()zwracana wartość jest odpowiednikiem prvalue T(), tjest inicjowana bezpośrednio przez T(), dokładnie tak, jak gdybyś to zrobił T t = T().

Jeśli prvalue zostanie użyte w jakikolwiek inny sposób, prvalue zmaterializuje tymczasowy obiekt, który zostanie użyty w tym wyrażeniu (lub odrzucony, jeśli nie ma wyrażenia). Więc jeśli to zrobisz const T &rt = Func();, prvalue zmaterializuje się tymczasowo (używając T()jako inicjalizatora), którego odniesienie zostanie zapisane rt, wraz ze zwykłymi tymczasowymi rozszerzeniami czasu życia.

Jedną z rzeczy, na które pozwala elision, jest zwracanie obiektów, które są nieruchome. Na przykład lock_guardnie można go skopiować ani przenieść, więc nie można mieć funkcji, która zwraca go według wartości. Ale z gwarantowaną eliminacją kopii, możesz.

Gwarantowana elekcja działa również przy bezpośredniej inicjalizacji:

new T(FactoryFunction());

Jeśli FactoryFunctionzwraca Twartość, to wyrażenie nie skopiuje wartości zwracanej do przydzielonej pamięci. Zamiast tego alokuje pamięć i używa przydzielonej pamięci jako pamięci wartości zwracanej bezpośrednio dla wywołania funkcji.

Dlatego funkcje fabryczne, które zwracają wartość, mogą bezpośrednio zainicjować przydzieloną pamięć sterty, nawet o tym nie wiedząc. Oczywiście pod warunkiem, że działają one wewnętrznie zgodnie z zasadami gwarantowanej eliminacji kopii. Muszą zwrócić prvalue typu T.

Oczywiście to też działa:

new auto(FactoryFunction());

Jeśli nie lubisz pisać nazw typów.


Ważne jest, aby pamiętać, że powyższe gwarancje działają tylko dla wartości prvalues. Oznacza to, że nie masz gwarancji, że zwrócisz nazwaną zmienną:

T Func()
{
   T t = ...;
   ...
   return t;
}

W tym przypadku tmusi nadal mieć dostępny konstruktor kopiowania / przenoszenia. Tak, kompilator może zdecydować o optymalizacji kopiowania / przenoszenia. Jednak kompilator musi nadal zweryfikować istnienie dostępnego konstruktora kopiowania / przenoszenia.

Więc nic się nie zmienia dla nazwanej optymalizacji wartości zwracanej (NRVO).

Nicol Bolas
źródło
1
@BenVoigt: Umieszczanie nietrywialnie kopiowalnych typów zdefiniowanych przez użytkownika w rejestrach nie jest wykonalną rzeczą, którą ABI może zrobić, niezależnie od tego, czy elision jest dostępny, czy nie.
Nicol Bolas
1
Teraz, gdy reguły są już publiczne, warto zaktualizować je za pomocą koncepcji „wartości prvalues ​​to inicjalizacje”.
Johannes Schaub - litb
6
@ JohannesSchaub-litb: Jest to „niejednoznaczne” tylko wtedy, gdy wiesz za dużo o szczegółach standardu C ++. W przypadku 99% społeczności C ++ wiemy, co oznacza „gwarantowana eliminacja kopii”. Tekst proponujący tę funkcję nosi nawet tytuł „Guaranteed Copy Elision”. Dodanie „poprzez uproszczone kategorie wartości” sprawia, że ​​jest to zagmatwane i trudne do zrozumienia dla użytkowników. Jest to również myląca nazwa, ponieważ te zasady tak naprawdę nie „upraszczają” reguł dotyczących kategorii wartości. Czy ci się to podoba, czy nie, termin „gwarantowana eliminacja kopii” odnosi się do tej funkcji i nic więcej.
Nicol Bolas
1
Tak bardzo chcę móc podnieść wartość i nosić ją ze sobą. Myślę, że to jest naprawdę (jednorazowe) std::function<T()>.
Yakk - Adam Nevraumont
1
@LukasSalich: To jest pytanie C ++ 11. Ta odpowiedź dotyczy funkcji C ++ 17.
Nicol Bolas