14 lutego 2014

[C++11] rvalue reference

Przebrnięcie przez move semantic wymaga wcześniejszego zapoznania się z rvalue reference (definicja wprowadzona już w C++98). Temat sprawił mi nie lada kłopot i z pewnością do niego powrócę jak tylko będę miał lepsze zrozumienie. Na razie notatki w takiej postaci i mam nadzieje, że nie ma w nich zbyt wielu błędów. Zbiór linków:

rvalue reference


Najpierw definicja lvalue/rvalue, które znalazłem pod pierwszym linkiem:
lvalue jest wyrażeniem, które odnosi się do jakiegoś miejsca w pamięci i możemy uzyskać adres tego miejsca korzystając z operatora &. rvalue jest wyrażeniem, które nie jest lvalue.
Można też stosować definicję "if it has a name ..." - link.
Jeżeli zmienna posiada nazwę to możemy uzyskać adres tej zmiennej, dzięki operatorowi &, wtedy wiem, że jest to lvalue. Nie możemy np. uzyskać nazwy literału (liczba 42), więc wiemy, że jest to rvalue.
lvalue reference mają trwały stan, tymczasem rvalue reference są albo literałami, albo tymczasowymi obiektami utworzonymi w czasie ewaluowania wyrażenia. Mają zatem ważne właściwości [C++Primer - s.533]:
  • Odnoszą się tylko do obiektów, które zostaną zaraz zniszczone
  • Nie ma innych użytkowników danego obiektu
Możemy więc bezpiecznie ukraść zasoby, do których odnosi się rvalue reference. Poniżej kilka operacji z wykorzystaniem tego mechanizmu.
#include <iostream>

using namespace std;

int main() {
    int i = 42;         // zmienna i jest lvalue

    int& lv1 = i;       // lvalue reference - referencja na i
//  int& lv2 = i * 5;   // i*5 to rvalue (stała) nie możemy zrobić referencji która pozwala na jej modyfikację

    const int& lv3 = i * 5;  // lvalue reference - możemy, ustawić referencję na stałą

//  int&&  rv1 = i;     // rvalue reference - nie może pokazywać na lvalue
    auto&& lv4 = i;     // lvalue reference - następuje dedukcja typu na "int&"

    int&& rv2 = 42;     // rvalue reference - 42 to literał
    int&& rv3 = i * 3;  // rvalue reference - wynik mnożenia to ulotna wartość (rvalue)

//  int&& rv4 = rv2;    // rv2 nie jest ulotne i jest ważne do końca zakresu!
    int&& rv5 = std::move(rv2); // rvalue reference - obietnica, że nie będziemy korzystać z rv2!
}

Reference collapsing i dedukcja typu


Logika podpowiada, że wszystko co ma postać T&& można by nazwać rvalue reference. Niestety nie jest to prawda, zależy to bowiem od kontekstu w jakim ta postać istnieje. Zgodnie z nowym standardem wszędzie tam, gdzie mamy do czynienia z dedukcją typu (szablony, auto, typedef oraz pewne konstrukcje decltype) i jest symbol &&, czasami możemy mieć rvalue reference, a czasami lvalue reference. Scott Meyers, proponuje swoją terminologię i nazywanie takich referencji universal reference, bowiem taka referencja może pokazywać na "wszystko": lvalue, rvalue, const, non-const, ...

Kiedy parametr funkcji jest typu T&&, a dodatkowo mamy do czynienia z dedukcją typu (bo funkcja jest szablonowa), to typ może ulec zmianie. Standard przewiduje trzy wyjątki (w książce dwa).
  • Dla argumentu będącego lvalue => T będzie lvalue referencją (czyli T&)
  • Dla argumentu będącego rvalue => T będzie po prostu T - o tej zasadzie wspomina Meyers w swoim wykładzie [strona 26], nie znalazłem tego w książce.
  • Jeśli stworzymy referencję do referencji (choć nie bezpośrednio) wtedy ta referencja się "zapada". rvalue reference zapada się do rvalue reference, w pozostałych przypadkach zapadanie jest do lvalue reference.
    • T& & => T&
    • T& && => T&
    • T&& & => T&
    • T&& && => T&&
Widać te zasady na przykładach poniżej, gdy np. dla zmiennej int v1, wydedukowany zostanie typ int&. Brakuje mi tylko przykładu kiedy T&& && zapada się do T&&. Może uda mi się kiedyś coś znaleźć.

Przykłady:
#include <iostream>

// T&& to universal reference
template <typename T>
void f(T&& t, std::string name, std::string descr) {
    std::cout << descr << std::endl;
    if (std::is_const<typename std::remove_reference<decltype(t)>::type>::value)
        std::cout << name << " is const" << std::endl;
    if (std::is_lvalue_reference<decltype(t)>::value)
        std::cout << name << " is lvalue reference" << std::endl;
    if (std::is_rvalue_reference<decltype(t)>::value)
        std::cout << name << " is rvalue reference" << std::endl;
    std::cout << std::endl;
}

int main() {
    f(42, "42", "f<int>(int &&) => f<int>(int&&)");

    int v1 = 42;          // lvalue
    f(v1, "v1", "f<int&>(int& &&) => f<int&>(int&)");

    const int v2 = 42;    // const lvalue
    f(v2, "v2", "f<const int&>(const int& &&) => f<const int&>(const int&)");

    int tmp = 42;         // lvalue
    int& v3 = tmp;        // lvalue reference
    f(v3, "v3", "f<int&>(int& &&) => f<int&>(int&)");

    const int& v4 = 42;   // lvalue reference na const
    f(v4, "v4", "f<const int&>(const in& &&) => f<const int&>(const int&)");

    int&& v5 = 42;        // rvalue reference
    if (std::is_rvalue_reference<decltype(v5)>::value)
        std::cout << "in main v5 if rvalue reference" << std::endl;
    f(v5, "v5", "f<int&>(int& &&) => f<int&>(int&)");

    const int&& v6 = 42;  // rvalue reference na const
    if (std::is_rvalue_reference<decltype(v6)>::value)
        std::cout << "in main v6 if rvalue reference" << std::endl;
    f(v6, "v6", "f<const int&>(const int& &&) => f<const int&>(const int&)");

    int v7 = 67;
    f(std::move(v7), "v7", "f<int>(int &&) => f<int>(int&&)");
}
Chociaż v5 i v6 to rvalue referencje, dzięki nazwie możemy uzyskać ich adres, więc w środku funkcji f(), będą to już lvalue referencje. Aby przekazać rvalue referencję do środka, trzeba skorzystać z std::move. Wynik:
f<int>(int &&) => f<int>(int&&)
42 is rvalue reference

f<int&>(int& &&) => f<int&>(int&)
v1 is lvalue reference

f<const int&>(const int& &&) => f<const int&>(const int&)
v2 is const
v2 is lvalue reference

f<int&>(int& &&) => f<int&>(int&)
v3 is lvalue reference

f<const int&>(const in& &&) => f<const int&>(const int&)
v4 is const
v4 is lvalue reference

in main v5 if rvalue reference
f<int&>(int& &&) => f<int&>(int&)
v5 is lvalue reference

in main v6 if rvalue reference
f<const int&>(const int& &&) => f<const int&>(const int&)
v6 is const
v6 is lvalue reference

f<int>(int &&) => f<int>(int&&)
v7 is rvalue reference
Z czterech form, dla których może następować dedukcja typu, trzy zachowują się tak samo: typ szablonowy, auto oraz typedef. Niestety decltype, posiada pewne wyjątki od tej reguły, ale w tej chwili, nie chce mi się w to zagłębiać. Może kiedyś.

Value categories


W C++11 wraz z wprowadzeniem move semantic, liczba kategorii została rozbudowana:
Zależności przedstawia poniższe drzewo:
@startuml "value_categories.png"

(expression) --> (glvalue)
(glvalue) --> (lvalue)
(glvalue) --> (xvalue)

(expression) --> (rvalue)
(rvalue) --> (xvalue)
(rvalue) --> (prvalue)

@enduml

Ich właściwości można podsumować w ten sposób:
  • glvalue ("generalized" lvalue)
    • mają nazwę
  • rvalue (nazwa historyczna, bo mogły się pojawić po prawej stronie operatora przypisania)
    • można z nich przenosić
  • lvalue (nazywa historyczna, bo mogły się pojawić po lewej stronie operatora przypisania)
    • mają nazwę i można z nich przenosić
  • xvalue ("eXpiring" value)
    • mają nazwę i można z nich przenosić
  • prvalue (pure rvalue)
    • nie mają nazwy i można z nich przenosić
O ile prvalue najbardziej mi przypomina starą definicję rvalue (nie ma nazwy), to teraz w jej skład (patrz drzwo) wchodzi również xvalue. Jest kilka przykładów tego wyrażenia:
  • wywołanie funkcji, której wynikiem jest rvalue reference (np. std::move(x) zwraca rvalue reference do x)
  • a[n], wyrażenie, gdzie a jest tablicą rvalue
  • a.m, wyrażenie, gdzie a jest rvalue i m jest składową non-static typu non-reference
  • wyrażenie rzutowania do rvalue reference (np. static_cast<char&&>(x))

Brak komentarzy:

Prześlij komentarz