Короткий вступ в rvalue-посилання

      Переклад статті «A Brief Introduction to Rvalue References», Howard E. Hinnant, Bjarne Stroustrup, Bronek Kozicki.
 
Rvalue посилання — маленьке технічне розширення мови C + +. Вони дозволяють програмістам уникати логічно непотрібного копіювання і забезпечувати можливість ідеальної передачі (perfect forwarding). Насамперед вони призначені для використання у високо продуктивних проектах і бібліотеках.
 
 

Введення

Цей документ дає первинне уявлення про нову функції мови C + + — rvalue посиланням. Це короткий навчальний керівництво, а не повна стаття. Для отримання додаткової інформації подивіться список посилань в кінці.
 
 

Rvalue посилання

Rvalue посилання — це складовою тип, дуже схожий на традиційну посилання в C + +. Щоб розрізняти ці два типи, ми будемо називати традиційну C + + посилання lvalue посилання. Коли буде зустрічатися термін посилання, то це відноситься до обох видів посилань, і до lvalue посиланнях, і до rvalue посиланнях.
 
За семантиці lvalue посилання формується шляхом поміщаючи & після деякого типу.
 
 
A a;
A& a_ref1 = a;  // это lvalue ссылка

Якщо після деякого типу помістити &&, то вийде rvalue посилання.
 
 
A a;
A&& a_ref2 = a;  // это rvalue ссылка

Rvalue посилання веде себе точно так само, як і lvalue посилання, за винятком того, що вона може бути пов'язана з тимчасовим об'єктом, тоді як lvalue пов'язати з часу (не константним) об'єктом можна.
 
 
A&  a_ref3 = A();  // Ошибка!
A&& a_ref4 = A();  // Ok

Питання: З чого б це могло нам знадобитися?!
 
Виявляється, що комбінація rvalue посилань і lvalue посилань — це те, що необхідно для легкої реалізації семантики переміщення (move semantics). Rvalue посилання може також використовуватися для досягнення ідеальної передачі (perfect forwarding), що раніше було невирішеною проблемою в C + +. Для більшості програмістів rvalue посилання дозволяють створити більш продуктивні бібліотеки.
 
 

Семантика переміщень (move semantics)

 
Усунення побічних копій
Копіювання може бути дорогим задоволенням. Наприклад, для двох векторів, коли ми пишемо
v2 = v1
, то звичайно це викликає виклик функції, виділення пам'яті і цикл. Це, звичайно, прийнятно, коли нам дійсно потрібні дві копії вектора, але в багатьох випадках це не так: ми часто копіюємо вектор з одного місця в інше, а потім видаляємо стару копію. Розглянемо:
 
 
template <class T> swap(T& a, T& b)
{
    T tmp(a);   // сейчас мы имеем две копии объекта a
    a = b;      // теперь у нас есть две копии объекта b
    b = tmp;    // а теперь у нас две копии объекта tmp (т.е. a)
}

Насправді нам не потрібні копії
a
або
b
, ми просто хотіли обміняти їх. Давайте спробуємо ще раз:
 
 
template <class T> swap(T& a, T& b)
{
    T tmp(std::move(a));
    a = std::move(b);
    b = std::move(tmp);
}

Цей виклик
move()
повертає значення об'єкта, переданого як параметр, але не гарантує збереження цього об'єкта. Приміром, якщо в якості параметра в
move()
передати
vector
, то можна обгрунтовано очікувати, що після роботи функції від параметра залишиться вектор нульової довжини, так як всі елементи будуть переміщені, а не скопійовані. Іншими словами, переміщення — це зчитування із стиранням (destructive read).
 
У даному випадку ми оптимізували
swap
спеціалізацією. Однак ми не можемо спеціалізувати кожну функцію, яка копіює великий об'єкт безпосередньо перед тим, як вона видаляє або перезаписує його. Це було б неконструктивно.
 
Головне завдання rvalue посилань полягає в тому, щоб дозволити нам реалізовувати переміщення без переписування коду і витрат часу виконання (runtime overhead).
 
 
Move
Функція
move
насправді виконує вельми скромну роботу. Її завдання полягає в тому, щоб прийняти або lvalue, або rvalue параметр, і повернути його як rvalue без виклику конструктора копіювання:
 
 
template <class T>
typename remove_reference<T>::type&&
move(T&& a)
{
    return a;
}

Тепер все залежить від клієнтського коду, де повинні бути перевантажені ключові функції (наприклад, конструктор копіювання і оператор привласнення), що визначають чи буде параметр lvalue або rvalue. Якщо параметр lvalue, то необхідно виконати копіювання. Якщо rvalue, то можна безпечно виконати переміщення.
 
 
Перевантаження для lvalue / rvalue
Розглянемо простий клас, який володіє ресурсом і також забезпечує семантику копіювання (конструктор копіювання і оператор привласнення). Наприклад,
clone_ptr
міг би володіти покажчиком і викликати у нього дорогий метод
clone()
для копіювання:
 
 
template <class T>
class clone_ptr
{
private:
    T* ptr;
public:
    // Конструктор
    explicit clone_ptr(T* p = 0) : ptr(p) {}
 
    // Деструктор
    ~clone_ptr() {delete ptr;}
 
    // Семантика копирования
    clone_ptr(const clone_ptr& p)
        : ptr(p.ptr ? p.ptr->clone() : 0) {}
 
    clone_ptr& operator=(const clone_ptr& p)
    {
        if (this != &p)
        {
            delete ptr;
            ptr = p.ptr ? p.ptr->clone() : 0;
        }
        return *this;
    }
 
    // Семантика перемещения
    clone_ptr(clone_ptr&& p)
        : ptr(p.ptr) {p.ptr = 0;}
 
    clone_ptr& operator=(clone_ptr&& p)
    {
        std::swap(ptr, p.ptr);
        return *this;
    }
 
    // Прочие операции
    T& operator*() const {return *ptr;}
    // ...
};

За винятком семантики переміщення,
clone_ptr
— це код, який можна знайти в сьогоднішніх книгах по C + +. Користувачі могли б використовувати
clone_ptr
так:
 
 
clone_ptr<base> p1(new derived);
// ...
clone_ptr<base> p2 = p1;  // и p2 и p1 владеют каждый своим собственным указателем

 
Зверніть увагу, що виконання конструктора копіювання або оператора присвоєння для
clone_ptr
є відносно дорогою операцією. Однак, коли джерело копії є rvalue, можна уникнути виклику потенційно дорогої операції
clone()
, крадучи покажчик rvalue (ніхто не помітить!). У семантиці переміщення конструктор переміщення залишає значення rvalue в створюваному об'єкті, а оператор присвоювання міняє місцями значення поточного об'єкта з об'єктом rvalue посилання.
 
Тепер, коли код намагається скопіювати rvalue
clone_ptr
, або якщо є явний дозвіл вважати джерело копії rvalue (використовуючи
std::move
), робота виконається набагато швидше.
 
 
clone_ptr<base> p1(new derived);
// ...
clone_ptr<base> p2 = std::move(p1); // теперь p2 владеет ссылкой, вместо p1

Для класів, складених з інших класів (або через включення, або через успадкування), конструктор переміщення і переміщує привласнення може легко бути реалізовано при використанні функції
std::move
.
 
 
class Derived
    : public Base
{
    std::vector<int> vec;
    std::string name;
    // ...
public:
    // ...
    // Семантика перемещения
    Derived(Derived&& x)              // объявлен как rvalue
        : Base(std::move(x)), 
          vec(std::move(x.vec)),
          name(std::move(x.name)) { }
 
    Derived& operator=(Derived&& x)   // объявлен как rvalue
    {
        Base::operator=(std::move(x));
        vec  = std::move(x.vec);
        name = std::move(x.name);
        return *this;
    }
    // ...
};

Кожен подоб'екти буде тепер оброблений як rvalue в конструкторі переміщення і операторі перемещающего присвоювання об'єкта. У
std::vector
і
std::string
операції переміщення вже реалізовані (точно так само, як і у нашого
clone_ptr
), які дозволяють уникнути значно дорожчих операцій копіювання.
 
Варто відзначити, що параметр
x 
оброблений як lvalue в операціях переміщення, незважаючи на те, що він оголошений як rvalue посилання. Тому необхідно використовувати
move(x)
замість просто
x
при передачі базового класу. Це ключовий механізм безпеки семантики переміщення, розробленої для запобігання випадкової спроби подвійного переміщення з деякою іменованої змінної. Всі переміщення відбуваються тільки з rvalues ​​або з явним приведенням до rvalue (за допомогою
std::move
). Якщо у змінної є ім'я, то це lvalue.
 
Питання: А як щодо типів, які не володіють ресурсами? (Наприклад,
std::complex
?)
 
У цьому випадку не потрібно проводити ніякої роботи. Конструктор копіювання вже оптимальний для копіювання з rvalue.
 
 

Переміщувані, але не копійовані типи

До деяких типів семантика копіювання не може бути застосована, але їх можна переміщати. Наприклад:
 
 
     
  • fstream
  •  
  • unique_ptr
    (не поділяє і не копируемое володіння)
  •  
  • Тип, що представляє потік виконання
  •  
Якщо такі типи робити переміщуваними (хоча вони лишаються не копійований), то зручність їх використання надзвичайно збільшується. Переміщуваний, але не копійований об'єкт може бути повернений за значенням з фабричного методу (патерн):
 
 
ifstream find_and_open_data_file(/* ... */);
...
ifstream data_file = find_and_open_data_file(/* ... */);  // Никаких копий!

У цьому прикладі базовий дескриптор файлу переданий з одного об'єкта в інший, тому що джерело
ifstream
є rvalue. У будь-якому момент часу є тільки один дескриптор файлу, і тільки один
ifstream
володіє ним.
 
Переміщуваний, але не копійований тип також може бути поміщений в стандартні контейнери. Якщо контейнеру необхідно "скопіювати" елемент всередині себе (наприклад, при реалокаціі
vector
), він просто перемістить його замість копіювання.
 
 
vector<unique_ptr<base>> v1, v2;
v1.push_back(unique_ptr<base>(new derived()));  // OK, перемещение без копирования
...
v2 = v1;             // Ошибка времени компиляции! Это не копируемый тип.
v2 = move(v1);       // Нормальное перемещение. Владение указателем будет передано v2.

Багато стандартні алгоритми отримують вигоду від переміщення елементів послідовності замість їх копіювання. Це не тільки забезпечує кращу продуктивність (як у випадку
std::swap
, реалізація якого описала вище), але і дозволяє цим алгоритмам працювати з некопіруемимі (але переміщуваними) типами. Наприклад, наступний код сортує
vector<unique_ptr<T>>
, грунтуючись на типі, який зберігається в розумному покажчику:
 
 
struct indirect_less
{
    template <class T>
    bool operator()(const T& x, const T& y)
        {return *x < *y;}
};
...
std::vector<std::unique_ptr<A>> v;
...
std::sort(v.begin(), v.end(), indirect_less());

Оскільки алгоритм сортування переміщує об'єкти
unique_ptr
, він буде використовувати
swap
(який більше не вимагає підтримки копируемое від об'єктів, значення яких він обмінює) або конструктор переміщення / оператор переміщує присвоювання. Таким чином, протягом всієї роботи алгоритму підтримується інваріант, за яким кожен зберігається об'єкт знаходиться у володінні тільки одного розумного покажчика. Якби алгоритм зробив спробу копіювання (наприклад, помилково програміста), то результатом була б помилка часу компіляції.
 
 

Ідеальна передача (perfect forwarding)

Розглянемо універсальний фабричний метод, який повертає
std::shared_ptr
для щойно створеного універсального типу. Такі фабричні методи цінні для інкапсуляції і локалізації виділення ресурсів. Очевидно, фабричний метод повинен приймати точно такий же набір параметрів, що і конструктор типу створюваного об'єкта. Зараз це може бути реалізовано так:
 
 
template <class T>
std::shared_ptr<T>
factory()   // версия без аргументов
{
    return std::shared_ptr<T>(new T);
}
 
template <class T, class A1>
std::shared_ptr<T>
factory(const A1& a1)   // версия с одним аргументом
{
    return std::shared_ptr<T>(new T(a1));
}
 
// все остальные версии

В інтересах стислості ми будемо фокусуватися на простої версії з одним параметром. Наприклад:
 
 
std::shared_ptr<A> p = factory<A>(5);

Питання: Що буде, якщо конструктор
T
отримує параметр по які константної посиланням?
 
У цьому випадку ми отримуємо помилку часу компіляції, оскільки константних параметр функції
factory
не зв'язуватиметься з неконстантним параметром конструктора типу
T
.
 
Для вирішення цієї проблеми можна використовувати неконстантний параметр у функції
factory
:
 
 
template <class T, class A1>
std::shared_ptr<T>
factory(A1& a1)
{
    return std::shared_ptr<T>(new T(a1));
}

Так набагато краще. Якщо тип з модифікатором
const
буде переданий
factory
, то константа буде виведена в шаблонний параметр (наприклад,
A1
) і потім належним чином передана конструктору
T
. Точно так само, якщо фабриці буде переданий неконстантний параметр, то він буде правильно переданий конструктору
T
як неконстанта. Насправді саме так найчастіше реалізується передача параметра (наприклад,
std::bind
).
 
Тепер розглянемо наступну ситуацію:
 
 
std::shared_ptr<A> p = factory<A>(5);    // Ошибка!
A* q = new A(5);                                          // OK

Цей приклад працював з першою версією
factory
, але тепер аргумент «5» викликає шаблон
factory
, який буде виведений як
int&
і згодом не зможе бути пов'язаним з rvalue «5». Таким чином, жодне рішення не можна вважати правильним, кожен страждає своїми проблемами.
 
Питання: А може зробити перевантаження для кожної комбінації
AI
& і
const AI
&?
 
Це дозволило б нам обробляти всі приклади, але призведе до експоненційної вартості: для нашого випадку з двома параметрами це вимагало б 4 перевантаження. Для фабрики з трьома параметрами ми потребували б 8 додаткових перевантаженнях. Для фабрики з чотирма параметрами треба було б вже 16 перевантажень і т.д. Це абсолютно не масштабується,.
 
Rvalue посилання пропонують просте і масштабується, цього завдання:
 
 
template <class T, class A1>
std::shared_ptr<T>
factory(A1&& a1)
{
    return std::shared_ptr<T>(new T(std::forward<A1>(a1)));
}

Тепер rvalue параметри можуть бути пов'язані з параметрами
factory
. Якщо параметр
const
, то він буде виведений в шаблонний тип параметра
factory
.
 
Питання: Що за функція
forward
використовується в цьому рішенні?
 
Як і
move
,
forward
— це проста стандартна бібліотечна функція, яка використовується, щоб виразити намір безпосередньо і явно, а не за допомогою потенційно загадкового використання посилань. Ми хочемо передати параметр
A1
, і просто заявляємо про це.
 
Тут,
forward
зберігає lvalue / rvalue параметр, який був переданий
factory
. Якщо
factory
був переданий rvalue, то за допомогою
forward
і конструктору
T
буде переданий rvalue. Точно так само, якщо lvalue параметр переданий
factory
, він же буде переданий конструктору
T
як lvalue.
 
Визначення функції
forward
може виглядати приблизно так:
 
 
template <class T>
struct identity
{
    typedef T type;
};
 
template <class T>
T&& forward(typename identity<T>::type&& a)
{
    return a;
}

 

Посилання

Оскільки одна з основних цілей цієї замітки стислість, деякі деталі були свідомо опущені. Тим не менш, тут покрито 95% знань по цій темі.
 
Для отримання подальшої інформації по семантики переміщення, такий як тести продуктивності, деталі переміщень, що не копійовані типи, та багато іншого дивіться N1377 .
 
Для повної інформації про обробку проблеми передачі дивіться N1385 .
 
Для подальшого вивчення rvalue посилань (крім семантики переміщення і ідеальної передачі), дивіться N1690 .
 
Для формулювань змін мови, необхідних rvalue посиланнями, дивіться N1952 .
 
Огляд по rvalue посиланнях і семантиці переміщення, дивіться N2027 .
 
Зведення по всім впливам rvalue посилань на стандартну бібліотеці, знаходиться в N1771 .
 
Пропозиції та формулювання для змін в бібліотеці, що вимагають використовувати в rvalue посилання, дивіться:
 Пропозиції распостранить rvalue посилання на неявний об'єктний параметр (this), дивіться N1821 .
  
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.