std::move

Jedną z ciekawszych i najbardziej przydatnych możliwości nowego standardu C++ jest przenoszenie obiektów. Co mam na myśli mówiąc, że dany obiekt może zostać przeniesiony? W dużym uproszczeniu będzie to oznaczać “nie skopiowany”. Funkcja szczególnie przydatna do uniknięcia zbędnego kopiowania obiektów tymczasowych.

Wyobraźmy sobie pewien abstrakcyjny przykład: klasa Bar wykorzystuje obiekty klasy Foo. W związku z tym przechowuje wewnętrznie obiekty tej klasy. W C++03 mogłoby to wyglądać następująco:

class Foo;

struct Bar
{
    explicit Bar(const Foo& f) : myFoo(f) {}
    Foo myFoo;
};

int main()
{
    Bar b((Foo()));
}

Problem powyższego rozwiązania tkwi w niepotrzebnym kopiowaniu. Obiekt klasy Foo zostanie najpierw utworzony ze swoim domyślnym konstruktorem, a następnie skopiowany już wewnątrz klasy Bar. Jednak to kopiowanie jest całkowicie niepotrzebne, bo oryginał już po chwili przestaje istnieć. Znacznie lepiej byłoby przesunąć informacje z takiego tymczasowego obiektu w nowe miejsce.

Wyobraźmy sobie takie przeniesienie informacji o obiekcie (a więc jego atrybutów) jako przeprowadzkę do nowego mieszkania. Dopiero co je kupiliśmy, wszystkie graty są już przeniesione, nic, tylko zburzyć ściany w poprzednim lokum, żeby zostawić za sobą wszystkie nieprzyjemne wspomnienia, prawda? Nie do końca. Przede wszystkim, poprzednie mieszkanie nie było nasze (bo wynajmowane od czasu studiów, jeszcze za pieniądze rodziców) i jego właściciel może chcieć je ponownie wynająć. Również musimy mieć na względzie to, żeby nasi znajomi nie próbowali się z nami kontaktować pod starym adresem, bo może ich spotkać niemiła niespodzianka.

Właśnie na coś takiego pozwala operacja przenoszenia obiektu, jedynie z pewnym zastrzeżeniem: oryginał musi być nadal ważnym obiektem w sensie możliwości jego użycia. Przecież, jeśli był utworzony na stosie, to w momencie jego zwijania obiekt musi zostać w normalny sposób usunięty. Być może też ktoś przechowuje referencję do takiego obiektu i chciałby go wywołać…

Niebezpieczne zabawy

Zatrzymajmy się na chwilę w tym miejscu. Przed chwilą poprzez analogię do przeprowadzki pokazałem, że próby odwoływania się do przeniesionego obiektu mogą dać niespodziewane (niezdefiniowane) wyniki, a teraz wspominam o tym, że ktoś mógłby chcieć się do takiego obiektu odwołać? Od razu powiem - nie powinno się tego robić. Sytuacja przypomina problemy z jedynym sprytnym wskaźnikiem udostępnianym przez bibliotekę standardową przed C++11, czyli std::auto_ptr. Problem z nim jest taki, że operacje, które wyglądają jak kopiowanie są tak naprawdę niejawnym przesunięciem. Jeśli zatem utworzony auto_ptr przekazać do funkcji, to poza nią traci on swoją ważność. Przykład:

void doSthWithPtr(std::auto_ptr<Foo> p);

int main()
{
    std::auto_ptr<Foo> fooPtr(new Foo);
    doSthWithPtr(foo);
    fooPtr->fooMethod(); // niepoprawne, przeniesione do zmiennej "p" funkcji doSthWithPtr
}

Nie jest to dobrze przemyślane rozwiązanie i w bardziej skomplikowanym kodzie często prowadzi do błędów. Z tego powodu w nowej wersji C++ auto_ptr został zastąpiony przez unique_ptr. Z drugiej strony zwracanie std::auto_ptr jako wyniku działania funkcji jest całkowicie poprawne i nie niesie z sobą żadnego ryzyka, a umożliwia automatyczne usuwanie opakowanego wskaźnika. Z tego czerpiemy naukę, że aby przenoszenie obiektów w ogóle miało jakiś sens, musimy być w stanie określić, kiedy jest ono bezpieczne, a kiedy nie. Jeśli zaś chcemy umożliwić przenoszenie obiektów nawet gdy nie jest to bezpieczne, należy robić to jawnie.

Referencja do r-wartości (rvalue reference)

Jedynymi bezpiecznymi - z punktu widzenia przenoszenia - instancjami danej klasy są tzw. r-wartości (rvalues), czyli obiekty tymczasowe istniejące jedynie przez jedno wyrażenie. Przykładem r-wartości jest wywołanie konstruktora bez tworzenia obiektu (jak w pierwszym przykładzie) albo wynik jakiegoś działania, np. dodawania (foo1 + foo2 zwróci trzeci obiekt wynikowy, który jest właśnie r-wartością). C++11 rozwiązuje problem rozpoznawania takich obiektów poprzez wprowadzenie nowego typu referencyjnego, rvalue reference. Aby powiedzieć, że w danym miejscu spodziewamy się referencji do r-wartości, używamy podwójnego ampersanda (“&&”) zapisywanego po nazwie typu. Dzięki takiemu zapewnieniu wiemy, że możemy bez wahania i do woli przenosić obiekt rezydujący pod daną zmienną.

Przykład sygnatury funkcji, która przyjmuje referencję do r-wartości jako argument: void doSth(Foo&& givenFoo) Zwracam przy tym uwagę na brak słowa kluczowego const przed nazwą typu, które często występuje wraz ze zwykłymi referencjami. Wprawdzie jest ono dozwolone i oczywiście istnieją stałe referencje do r-wartości, lecz z powodu ich specyfiki, miałyby one znikome znaczenie. Jedynym zastosowaniem dla const rvalue reference, które potrafię wymyślić, jest przeładowanie funkcji w celu zapewnienia, że jej parametr nie zostanie przekazany przez r-wartość:

void doSth(const Foo& sth) {
    // kod
}
void doSth(const Foo&&) = delete; // błąd kompilacji np. przy wywołaniu doSth((Foo());

Należy przy tym zaznaczyć, że obiektu przekazanego przez referencję do r-wartości wcale nie musimy przenosić! Możemy go równie dobrze skopiować, czy też wykonać na nim jakiekolwiek inne operacje. Nie jest to jednak zalecane z dwóch powodów. Po pierwsze sztucznie się ograniczamy, bo używamy wówczas takiej zmiennej jak zwykłej referencji, lecz przy próbie przekazania l-wartości (lvalue (czyli odwrotności r-wartości) kompilator zwróci błąd. Po drugie piszemy kod całkowicie wbrew konwencji sugerując przy tym, że dany obiekt jest przenoszony, pomimo że nie jest.

Konstruktor przenoszący (move constructor)

Miejscem, w którym decydujemy o sposobie przenoszenia obiektu jest konstruktor przenoszący. Jest to nowy typ konstruktora wprowadzony w C++11, którego sygnatura jest następująca: Foo::Foo(Foo&& that). To w nim przenosimy informacje o obiekcie that do nowego obiektu. Można więc powiedzieć, że w ten sposób zmieniane jest posiadanie zarządzanych zasobów. Pamiętamy bowiem o tym, że przeniesienie zasobów wiąże się z ich zresetowaniem w oryginalnym obiekcie (w przeciwnym wypadku byłoby to kopiowanie). Najlepiej to zostanie zobrazowane na przykładzie:

template <typename MyType>
class Foo
{
    MyType* myObj;

public:
    Foo(Foo&& that) : myObj(that.myObj)
    {
        that.myObje = nullptr;
    }
};

W powyższym przykładzie po prostu podmieniliśmy wskaźnik na obiekt typu MyType i zdezaktualizowaliśmy go w oryginale. Jest to niezwykle ważne, bo w momencie wywołania destruktora, oryginał mógłby próbować go usunąć. Próba usunięcia wskaźnika NULL jest natomiast niegroźna.

Fizycznie zmieniliśmy zatem jedynie wartości dwóch wskaźników, co jest operacją niezwykle tanią, gdyż uniknęliśmy kosztownego kopiowania obiektu typu MyType, który nie wiadomo co tam w sobie ma, być może kontenery przechowujące tysiące innych obiektów. Jednocześnie zmieniliśmy właściciela zasobów, gdyż oryginalny obiekt Foo przestał mieć o nich jakiekolwiek pojęcie.

Przykład ten moglibyśmy również zaimplementować przy użyciu idiomu move-and-swap (wariacja copy-and-swap), czyli przenieś i podmień. Wykorzystuje on fakt, że przy konstruowaniu obiektu, do którego będziemy przenosić zarządzane zasoby, wpierw ustawiamy je na jakieś wartości początkowe. Wystarczy zatem podmienić te wartości początkowe z wartościami przenoszonymi (tj. zamienić je miejscami) i efekt będzie ten sam.

// move-and-swap
template <typename MyType>
class Foo {
    MyType* myObj;

public:
    friend swap(Foo& first, Foo& second)
    {
        std::swap(first.myObj, second.myObj);
    }

    Foo(Foo&& that) : myObj(nullptr)
    {
        swap(*this, that);
    }

    Foo& operator=(Foo&& rhs)
    {
        swap(*this, rhs);
        return *this;
    }
};

Tym razem zaimplementowany został także operator przypisania wspierający semantykę przenoszenia. Dzięki wykorzystaniu zaprzyjaźnionej operacji swap zaoszczędziliśmy również przy pisaniu kodu, gdyż jest ona jedynym miejscem gdzie faktycznie następuje podmiana. Taka implementacja ma również inne zalety, jednak powiem o nich być może innym razem. ## Przenoszenie L-wartości

Może się zdarzyć tak, że w celu zaoszczędzenia cykli procesora poświęconych na kopiowanie, będziemy chcieli przenieść obiekt będący l-wartością, czyli nie będący obiektem tymczasowym. Wiemy bowiem, że już za chwilę dany obiekt przestanie mieć dla nas w danym miejscu znaczenie i chcielibyśmy zmienić jego właściciela, biorąc za to pełną odpowiedzialność. Wobec tego musimy jawnie zadeklarować chęć przeniesienia l-wartości. Standard C++11 przewidział taką możliwość i udostępnia w tym celu funkcję pomocniczą dostępną w pliku nagłówkowym <utility>, std::move. Funkcja ta dzięki rzutowaniu zwraca dowolny dostarczony jej argument jako referencję do r-wartości. Wywołując zatem std::move w jawny sposób mówimy, że wyrażamy zgodę na przeniesienie danego obiektu wraz ze wszystkimi konsekwencjami tego działania. Użycie wygląda w sposób następujący:

Foo myObj;
Foo newObj(std::move(myObj));

Wróćmy jeszcze na chwilę do klasy Bar, która przechowuje swój egzemplarz obiektu Foo przekazywany w konstruktorze. Moglibyśmy przeładować konstruktor jednoargumentowy klasy Bar, aby przyjmował również obiekty przez referencję do r-wartości. W ten sposób można udostępnić użytkownikowi klasy zarówno możliwość kopiowania obiektów Foo jak i ich przenoszenia. Należy jedynie pamiętać o tym, że wewnątrz konstruktora przenoszącego obiekt przekazany przez referencję do r-wartości zostanie przypisany do pewnej zmiennej referencyjnej i stanie się l-wartością. Należy zatem ponownie jawnie zadeklarować chęć jego przeniesienia poprzez użycie funkcji std::move, gdyż w przeciwnym razie zostałby skopiowany. Najlepiej, jeśli pokażę to na przykładzie:

explicit Bar(Foo&& f) : myFoo(f) {} // 1
explicit Bar(Foo&& f) : myFoo(std::move(f)) {} // 2

W wersji 1 konstruktora przenoszącego spowodujemy skopiowanie obiektu f do myFoo. f jest l-wartością - nie jest to zmienna tymczasowa, która zaraz ginie, może natomiast być użyta w całym ciele konstruktora. Jeśli zależy nam na faktycznym przeniesieniu obiektu, powinniśmy jawnie to zadeklarować, tak jak ma to miejsce w wersji 2 konstruktora przenoszącego. Zwróćmy uwagę, że spowoduje to wywołanie konstruktora przenoszącego klasy Foo, który w analogiczny sposób może chcieć przenosić zasoby, którymi zarządza. W ten sposób może zostać wywołana cała kaskada konstruktorów przenoszących.

Domyślny konstruktor przenoszący

Co, jeśli w którymś momencie owej kaskady natrafimy na obiekt, który nie ma zaimplementowanego konstruktora przenoszącego? Na szczęście tutaj do gry włącza się kompilator, który domyślnie wygeneruje konstruktor przenoszący dla większości obiektów (a także przenoszącą wersję operatora przypisania). Generacja taka nie zostanie on jedynie wykonana, gdy autor danej klasy jawnie określi, że jej sobie nie życzy (X(X&&) = delete;) lub jawnie destruktor, konstruktor kopiujący lub kopiujący operator przypisania. Z drugiej strony, jeśli jawnie określimy destruktor, konstruktor przenoszący lub przenoszący operator przypisania, kompilator nie wygeneruje konstruktora kopiującego i kopiującego operatora przypisania. Może się również zdarzyć tak, że określimy pewien konstruktor przenoszący, ale jeśli będzie on inny niż domyślny, kompilator nadal będzie takowy generował.

Aby zmusić kompilator do wygenerowania np. domyślnego konstruktora przenoszący dla klasy, w której określiliśmy konstruktor kopiujący, musimy zapisać to następująco:

struct X {
    X(const X& that);
    X& operator=(X rhs);

    X(X&&) = default;
};

Mam nadzieję, że semantyka przenoszenia w C++11, jeśli do tej pory nie była do końca zrozumiała, stała się dzięki temu artykułowi nieco jaśniejsza. Jest to bowiem doskonała technika optymalizacyjna, warta stosowania. Warto tu jeszcze tylko nadmienić, że biblioteka standardowa została przystosowana do przenoszenia obiektów. Zatem, wywołując std::vector::push_back możemy przekazać referencję do r-wartości i wektor przeniesie przekazany w ten sposób obiekt zamiast go kopiować. Warto o tym poczytać i zorientować się, w których miejscach możemy dodatkowo zoptymalizować nasze programy.