Коли зробити тип нерухливим в C ++ 11?


127

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

Коли я повинен (або мені це подобається) зробити клас нерухливим на C ++ 11?

( Інші причини, ніж проблеми сумісності з існуючим кодом, тобто.)


2
прискорення завжди на крок попереду - «дорогі типи для переміщення» ( boost.org/doc/libs/1_48_0/doc/html/container/move_emplace.html )
SChepurin

1
Я думаю, що це дуже гарне та корисне запитання ( +1від мене) з дуже ретельною відповіддю від Герба (або його близнюка, як здається ), тому я зробив це запитання до FAQ. Якщо хтось заперечує проти того , щоб я просто пінгнув мене в залі , тож це можна обговорити там.
sbi

1
Рухомі класи AFAIK все ще можуть бути предметом нарізки, тому має сенс заборонити переміщення (і копіювання) для всіх поліморфних базових класів (тобто для всіх базових класів з віртуальними функціями).
Філіп

1
@Mehrdad: Я просто кажу, що "T має конструктор переміщення", а " T x = std::move(anotherT);бути законним" - не рівнозначно. Останнє - це запит на переміщення, який може потрапити назад на копіюючий копіювальник у випадку, якщо T не має переміщувача. Отже, що саме означає "рухомий"?
sellibitze

1
@Mehrdad: Перегляньте стандартний розділ бібліотеки C ++ про те, що означає "MoveConstructible". Деякий ітератор може не мати конструктора переміщення, але він все ще є MoveConstructible. Слідкуйте за різними визначеннями "рухомих" людей на увазі.
sellibitze

Відповіді:


110

Відповідь Трави (перш ніж він був відредагований) на насправді дав хороший приклад такого типу , який не повинен бути рухомим: std::mutex.

Нативний тип мутексу ОС (наприклад, pthread_mutex_tна платформах POSIX) може не бути "інваріантним розташуванням", тобто адреса об'єкта є частиною його значення. Наприклад, ОС може зберігати список покажчиків на всі ініціалізовані об'єкти mutex. Якщо std::mutexв якості члена даних міститься вбудований тип мутексу ОС, а адреса основного типу повинна залишатися фіксованою (оскільки ОС підтримує список покажчиків на її мутекси), то або std::mutexдоведеться зберігати нативний тип мутексу у купі, щоб він залишався на те саме місце розташування при переміщенні між std::mutexоб'єктами або std::mutexпереміщення не повинно Збереження його на купі неможливо, тому std::mutexщо у constexprконструктора є конструктор і він повинен мати право на постійну ініціалізацію (тобто статичну ініціалізацію), так що глобальнийstd::mutexгарантовано буде побудовано до запуску програми, тому її конструктор не може використовувати new. Тож єдиний варіант, який залишився - std::mutexце бути нерухомим.

Те ж міркування стосується інших типів, які містять щось, що вимагає фіксованої адреси. Якщо адреса ресурсу повинна залишатися фіксованою, не переміщуйте її!

Є ще один аргумент щодо не переміщення, std::mutexякий полягає в тому, що це було б дуже важко зробити це безпечно, тому що вам потрібно знати, що ніхто не намагається заблокувати мютекс у той момент, коли він переміщується. Оскільки мутекси є одним із будівельних блоків, які ви можете використовувати для запобігання перегонів даних, було б прикро, якби вони не були безпечними проти самих перегонів! З нерухомим std::mutexви знаєте, що єдине, що може зробити хтось після того, як він був побудований і перед його знищенням, - це заблокувати та розблокувати, і ці операції гарантовано, що вони є безпечними для потоків, і не вводять перегони даних. Цей самий аргумент застосовується і до std::atomic<T>об'єктів: якщо б їх не вдалося перенести атомним шляхом, неможливо було б безпечно переміщувати їх, інший потік може намагатися викликатиcompare_exchange_strongна об'єкт прямо в той момент, коли він переміщується. Отже, ще один випадок, коли типи не повинні бути рухомими, це те, що вони є низькорівневими будівельними блоками безпечного одночасного коду і повинні забезпечувати атомність усіх операцій над ними. Якщо значення об'єкта може бути переміщено до нового об'єкта в будь-який час, вам потрібно буде використовувати атомну змінну для захисту кожної атомної змінної, щоб ви знали, чи безпечно це використовувати або переміщено ... та атомну змінну для захисту що атомна змінна і так далі ...

Думаю, я б узагальнив, щоб сказати, що коли об'єкт - це просто чистий фрагмент пам'яті, а не тип, який виступає в якості власника для значення або абстрагування значення, його немає сенсу переміщувати. Основні типи, такі як intнеможливо рухатись: переміщення їх - це лише копія. Ви не можете вирвати нутрощі з int, ви можете скопіювати його значення, а потім встановити його в нуль, але це все-таки intзначення, це лише байти пам'яті. Але intвсе ще рухомиймовними термінами, оскільки копія є дійсною операцією переміщення. Однак для типів, що не підлягають копіюванню, якщо ви не хочете або не можете перемістити частину пам'яті, а також не можете скопіювати її значення, то це неможливо перемістити. Мутекс або атомна змінна - це певне місце пам’яті (оброблене спеціальними властивостями), тому не має сенсу переміщуватися, а також не підлягає копіюванню, тому воно не є рухомим.


17
+1 менш екзотичний приклад того, що неможливо перемістити, оскільки в ньому є спеціальна адреса - вузол у спрямованій структурі графа.
Potatoswatter

3
Якщо мьютекс неможливо скопіювати і не переміщувати, як я можу скопіювати або перемістити об’єкт, що містить мутекс? (Як і безпечний клас для потоків із власним mutex для синхронізації ...)
tr3w

4
@ tr3w, ви не можете, якщо ви не створите мютекс на купі і не потримаєте його за допомогою унікального_ptr або подібного
Jonathan Wakely

2
@ tr3w: Ви б просто не перемістили весь клас, крім частини мютексу ?
користувач541686

3
@BenVoigt, але новий об’єкт матиме власну мютекс. Я думаю, що він має на увазі виконання операцій переміщення, визначених користувачем, які переміщують усіх членів, крім члена mutex. То що робити, якщо старий об’єкт закінчується? З ним закінчується мітекс.
Jonathan Wakely

57

Коротка відповідь: Якщо тип можна скопіювати, він також повинен бути переміщеним. Однак реверс не відповідає дійсності: деякі типи, подібні std::unique_ptrдо переміщення, все ж не має сенсу копіювати їх; це природно лише рухомі типи.

Трохи довша відповідь випливає ...

Існує два основних типи (серед інших більш спеціальних видів, таких як риси):

  1. Ціньоподібні типи, такі як intабо vector<widget>. Вони представляють значення, і вони, природно, можуть бути скопійовані. У C ++ 11, як правило, слід думати про рух як оптимізацію копії, і тому всі типи, що копіюються, природно повинні бути переміщеними ... переміщення - це лише ефективний спосіб робити копію у часто поширеному випадку, який ви не робите ' t вже не потребуватиме оригінальний об’єкт і просто збирається його знищити.

  2. Довідкові типи, що існують в ієрархіях успадкування, таких як базові класи та класи з віртуальними або захищеними функціями членів. Вони, як правило, утримуються покажчиком або посиланням, часто є base*або base&, і тому не забезпечують побудови копії, щоб уникнути нарізки; якщо ви хочете отримати ще один об'єкт, як вже існуючий, ви зазвичай викликаєте віртуальну функцію, як clone. Їм не потрібно побудувати або призначити переміщення з двох причин: їх не можна скопіювати, і вони вже мають ще більш ефективну природну операцію "переміщення" - ви просто скопіюєте / перемістіть вказівник на об'єкт, а сам об'єкт не робить доведеться взагалі перейти на нове місце пам'яті.

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



6
Так, я перейшов із використання одного облікового запису Google OAuth на інший і не можу потрудитися шукати спосіб злити два входи, які мені дають тут. (Ще один аргумент проти OAuth серед набагато більш переконливих.) Я, ймовірно, більше не буду використовувати інший, тож це те, що я зараз використовуватиму для випадкової публікації SO.
Трава Саттер

7
Я подумав, що std::mutexце нерухомість, оскільки текстові файли POSIX використовуються за адресою.
Щеня

9
@SChepurin: Власне, тоді це називається HerbOverflow.
sbi

26
Це отримує багато оновлень, ніхто не помітив, що це говорить, коли тип повинен бути лише переміщенням, що не в цьому питання? :)
Jonathan Wakely

18

Насправді, коли я шукав навколо, я виявив, що в C ++ 11 типи не є рухомими:

  • всі mutexтипи ( recursive_mutex, timed_mutex, recursive_timed_mutex,
  • condition_variable
  • type_info
  • error_category
  • locale::facet
  • random_device
  • seed_seq
  • ios_base
  • basic_istream<charT,traits>::sentry
  • basic_ostream<charT,traits>::sentry
  • всі atomicтипи
  • once_flag

Мабуть, існує дискусія про Clang: https://groups.google.com/forum/?fromgroups=#!topic/comp.std.c++/pCO1Qqb3Xa4


1
... ітератори не повинні бути рухомими ?! Що чому?
користувач541686

так, я думаю, його iterators / iterator adaptorsслід відредагувати, оскільки C ++ 11 має move_iterator?
billz

Гаразд зараз я просто розгублений. Ви говорите про Ітератор , які переміщують свої цілі , або про переміщення ітераторів самих ?
користувач541686

1
Так і є std::reference_wrapper. Гаразд, інші справді здаються не рухомими.
Крістіан Рау

1
Вони , здається, діляться на три категорії: паралелізм пов'язані типи 1. низькорівневі (Atomics, м'ютекси), 2. поліморфні базові класи ( ios_base, type_info, facet), 3. сортували дивні речі ( sentry). Напевно, єдині непорушні класи, які пише середній програміст, знаходяться у другій категорії.
Філіп

0

Ще одна причина, яку я знайшов - продуктивність. Скажімо, у вас клас "a", який має значення. Ви хочете вивести інтерфейс, який дозволяє користувачеві змінювати значення протягом обмеженого часу (для області).

Спосіб досягти цього полягає в поверненні об'єкта "охорона діапазону" з "а", який повертає значення в його деструктор, наприклад:

class a 
{ 
    int value = 0;

  public:

    struct change_value_guard 
    { 
        friend a;
      private:
        change_value_guard(a& owner, int value) 
            : owner{ owner } 
        { 
            owner.value = value;
        }
        change_value_guard(change_value_guard&&) = delete;
        change_value_guard(const change_value_guard&) = delete;
      public:
        ~change_value_guard()
        {
            owner.value = 0;
        }
      private:
        a& owner;
    };

    change_value_guard changeValue(int newValue)
    { 
        return{ *this, newValue };
    }
};

int main()
{
    a a;
    {
        auto guard = a.changeValue(2);
    }
}

Якби я зробив change_value_guard рухомим, я повинен був би додати його "if" до свого деструктора, який би перевірив, чи не було переміщено захист - це додатково, якщо і вплив на продуктивність.

Так, звичайно, це може бути оптимізовано будь-яким розумним оптимізатором, але все-таки приємно, що мова (для цього потрібен C ++ 17, хоча для повернення непорушного типу потрібна гарантована копія) платити за це, якщо ми не збираємось пересувати охорону, окрім того, як повернути його із створеної функції (принцип dont-pay-for-what-you-dont-use).

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