Мотивація та використання конструкторів переміщення в C ++


17

Нещодавно я читав про конструктори переміщень на C ++ (див., Наприклад, тут ) і намагаюся зрозуміти, як вони працюють і коли я повинен їх використовувати.

Наскільки я розумію, конструктор рухів використовується для полегшення продуктивності, спричиненої копіюванням великих об'єктів. На сторінці вікіпедії сказано: "Хронічна проблема продуктивності C ++ 03 - це дорогі та непотрібні глибокі копії, які можуть відбуватися неявно, коли об'єкти передаються за значенням".

Я зазвичай вирішую такі ситуації

  • шляхом передачі об'єктів посиланням, або
  • за допомогою розумних покажчиків (наприклад, boost :: shared_ptr) для проходження навколо об'єкта (смарт-покажчики копіюються замість об'єкта).

Які ситуації, в яких вищевказаних двох прийомів недостатньо, а використання конструктора переміщення зручніше?


1
Окрім того, що семантика переміщення може досягти набагато більшого (як сказано у відповідях), не слід запитувати, які ситуації, коли проходження посилання або за допомогою розумного вказівника недостатньо, але якщо ці методи справді найкращий і чистий спосіб зробити це (бог, остерігайся shared_ptrпросто заради швидкого копіювання), і якщо семантика переміщення може досягти того ж, майже без кодування, семантики та чистоти-покарання.
Кріс каже, що повернеться до Моніки

Відповіді:


16

Переміщення семантики вводить цілий вимір у C ++ - це не просто для того, щоб ви могли дешево повернути значення.

Наприклад, без рухової семантики std::unique_ptrне працює - подивіться на те std::auto_ptr, який був устареним із введенням семантики переміщення та видалений у C ++ 17. Переміщення ресурсу сильно відрізняється від його копіювання. Це дозволяє передати право власності на унікальний предмет.

Наприклад, не будемо розглядати std::unique_ptr, оскільки це досить добре обговорюється. Давайте подивимось, скажімо, на Vertex Buffer Object у OpenGL. Вершинний буфер представляє пам’ять на графічному процесорі - його потрібно розподілити та розмістити за допомогою спеціальних функцій, можливо, мають жорсткі обмеження щодо того, як довго він може жити. Також важливо, щоб ним користувався лише один власник.

class vertex_buffer_object
{
    vertex_buffer_object(size_t size)
    {
        this->vbo_handle = create_buffer(..., size);
    }

    ~vertex_buffer_object()
    {
        release_buffer(vbo_handle);
    }
};

void create_and_use()
{
    vertex_buffer_object vbo = vertex_buffer_object(SIZE);

    do_init(vbo); //send reference, do not transfer ownership

    renderer.add(std::move(vbo)); //transfer ownership to renderer
}

Тепер це можна зробити за допомогою std::shared_ptr- але цим ресурсом не можна ділитися. Це робить заплутаним використання спільного вказівника. Можна використовувати std::unique_ptr, але це все ще вимагає семантики переміщення.

Очевидно, я не реалізував конструктор рухів, але ви зрозуміли цю ідею.

Тут важливо те, що деякі ресурси не підлягають копіюванню . Ви можете проходити навколо покажчиків, а не переміщуватися, але якщо ви не використовуєте унікальний_ptr, виникає проблема власності. Варто бути максимально зрозумілим щодо того, якою є ціль коду, тому конструктор ходу - це, мабуть, найкращий підхід.


Дякую за відповідь. Що буде, якби тут використали загальний покажчик?
Джорджо

Я намагаюся відповісти собі: використання загального покажчика не дозволить контролювати термін експлуатації об’єкта, хоча це вимога, що об’єкт може жити лише певний час.
Джорджіо

3
@Giorgio Ви можете використовувати спільний покажчик, але це було б семантично неправильно. Неможливо поділити буфер. Крім того, це по суті змусить вас передати покажчик на покажчик (оскільки vbo в основному є унікальним вказівником на пам'ять GPU). Хтось, хто перегляне ваш код пізніше, може запитати "Чому тут спільний покажчик? Це спільний ресурс? Це може бути помилка! '. Краще бути максимально зрозумілим, яким був початковий намір.
Макс

@ Giorgio Так, це також є частиною вимоги. Коли "рендерінг" в цьому випадку хоче розмістити деякий ресурс (можливо, недостатньо пам'яті для нових об'єктів на графічному процесорі), не повинно бути жодної іншої обробки пам'яті. Використання shared_ptr, що виходить за рамки, спрацює, якщо ви не тримаєте його деінде, але чому б не зробити це абсолютно очевидним, коли зможете?
Макс

@Giorgio Перегляньте мою редакцію для ще однієї спроби уточнення.
Макс

5

Семантика переміщення - це не обов'язково все настільки покращення, коли ви повертаєте значення - і коли / якщо ви використовуєте shared_ptr(або щось подібне), ви, ймовірно, передчасно песимізуєте. Насправді майже всі досить сучасні компілятори роблять те, що називається оптимізацією повернутого значення (RVO) та оптимізацією іменованого повернення (NRVO). Це означає , що , коли ви повертаєте значення, а на самому ділі копіювання значення на все, вони просто передають прихований покажчик / посилання на те, де значення буде призначено після повернення, і функція використовує це для створення значення, де воно буде в кінцевому підсумку. Стандарт C ++ включає в себе спеціальні положення, які дозволяють це зробити, тому навіть якщо (наприклад) ваш конструктор копій має видимі побічні ефекти, не потрібно використовувати конструктор копій для повернення значення. Наприклад:

#include <vector>
#include <numeric>
#include <iostream>
#include <stdlib.h>
#include <algorithm>
#include <iterator>

class X {
    std::vector<int> a;
public:
    X() {
        std::generate_n(std::back_inserter(a), 32767, ::rand);
    }

    X(X const &x) {
        a = x.a;
        std::cout << "Copy ctor invoked\n";
    }

    int sum() { return std::accumulate(a.begin(), a.end(), 0); }
};

X func() {
    return X();
}

int main() {
    X x = func();

    std::cout << "sum = " << x.sum();
    return 0;
};

Основна ідея тут досить проста: створити клас з достатньою кількістю вмісту, ми скоріше уникаємо його копіювання, якщо можливо ( std::vectorми заповнюємо 32767 випадковими вставками). У нас є явний копіюючий копій, який показує нам, коли / якщо він буде скопійований. У нас також є трохи більше коду, щоб зробити щось із випадковими значеннями в об'єкті, тому оптимізатор не буде (принаймні легко) усунути все про клас тільки тому, що він нічого не робить.

Потім у нас є якийсь код для повернення одного з цих об'єктів з функції, а потім використовуємо підсумовування, щоб переконатися, що об’єкт справді створений, а не просто повністю ігнорований. Коли ми запускаємо його, принаймні з останніми / сучасними компіляторами, ми виявляємо, що конструктор копій, про який ми писали, ніколи не працює - і так, я впевнений, що навіть швидка копія з a shared_ptrвсе ще повільніше, ніж копіювання не робиться зовсім.

Переміщення дозволяє зробити досить багато речей, які ви просто не могли (без них) зробити без них. Розгляньте частину "злиття" зовнішнього сортування злиття - у вас, скажімо, 8 файлів, які ви збираєтеся об'єднати разом. В ідеалі ви хотіли б помістити всі 8 цих файлів у vector- але оскільки vector(на C ++ 03) потрібно вміти копіювати елементи, а ifstreams неможливо скопіювати, ви застрягли з деякими unique_ptr/ shared_ptr, або щось із цього наказу, щоб мати змогу поставити їх у вектор. Зауважте, що навіть якщо (наприклад) ми reserveпомістимо простір, vectorщоб ми впевнені, ifstreamщо їх ніколи насправді не буде скопійовано, компілятор цього не знатиме, тому код не буде компілюватися, хоча ми знаємо, що конструктор копій ніколи не буде застосовується все одно.

Незважаючи на те, що його все ще неможливо скопіювати, у C ++ 11 ifstream можна перемістити. У цьому випадку об'єкти, ймовірно, ніколи не будуть переміщені, але той факт, що вони можуть бути при необхідності, підтримує компілятор щасливим, тому ми можемо розміщувати наші ifstreamоб’єкти vectorбезпосередньо, без жодних злому розумних покажчиків.

Вектор, який розширюється, є досить пристойним прикладом часу, який семантика переміщення дійсно може бути / корисний, хоча. У цьому випадку RVO / NRVO не допоможе, оскільки ми не маємо справу з поверненим значенням функції (або чим-небудь дуже схожим). У нас є один вектор, який містить деякі об'єкти, і ми хочемо перемістити ці об'єкти в новий, більший шматок пам'яті.

У C ++ 03 це було зроблено шляхом створення копій об'єктів у новій пам'яті, а потім знищення старих об'єктів у старій пам'яті. Однак зробити ці копії просто для того, щоб викинути старі, було марною тратою часу. У C ++ 11 ви можете очікувати їх переміщення. Зазвичай це дозволяє нам, по суті, робити дрібну копію замість (як правило, набагато повільнішої) глибокої копії. Іншими словами, за допомогою рядка або вектора (лише для кількох прикладів) ми просто копіюємо вказівник (и) в об'єкти, замість того, щоб робити копії всіх даних, на які посилаються вказівники.


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

@Giorgio: Так, це майже правильно. Мова насправді не додає семантику переміщення; він додає посилання на оцінку. Посилання на оцінку (очевидно, що достатньо) може прив'язуватися до rvalue, і в цьому випадку ви знаєте, що безпечно "вкрасти" внутрішнє представлення даних і просто скопіювати його покажчики, а не робити глибоку копію.
Джеррі Труну

4

Поміркуйте:

vector<string> v;

Додаючи рядки до v, він розширюватиметься за потребою, і в кожному перерозподілі рядки потрібно буде скопіювати. Це стосується конструкторів, що рухаються, це в основному не проблема.

Звичайно, ви також можете зробити щось на кшталт:

vector<unique_ptr<string>> v;

Але це буде добре тільки тому, що std::unique_ptrреалізує конструктор переміщення.

Використовувати std::shared_ptrмає сенс лише у (рідкісних) ситуаціях, коли ви фактично маєте спільне право власності.


але що робити, якщо замість stringнас був екземпляр, Fooде він має 30 членів даних? unique_ptrВерсію не буде ефективнішою?
Василіс

2

Повернені значення - це те, де я найчастіше хотів би передати значення замість якогось посилання. Бути в змозі швидко повернути об’єкт «на стек» без масивного штрафу за продуктивність було б непогано. З іншого боку, це не особливо важко обійти (спільні вказівники просто такі прості у використанні ...), тому я не впевнений, що дійсно варто зайву роботу над моїми об'єктами, щоб мати можливість це зробити.


Я також зазвичай використовую смарт-покажчики для обгортання об'єктів, які повертаються з функції / методу.
Джорджо

1
@Giorgio: Це, безумовно, і примхливо, і повільно.
DeadMG

Сучасні компілятори повинні виконати автоматичне переміщення, якщо ви повернете простий об’єкт на стеці, тому немає необхідності в спільних ptrs тощо.
Крістіан Северин,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.