Когда в C ++ 11 сделать тип неподвижным?

127

Я был удивлен, что это не появилось в моих результатах поиска, я подумал, что кто-то спросил бы об этом раньше, учитывая полезность семантики перемещения в C ++ 11:

Когда мне нужно (или это хорошая идея) сделать класс неподвижным в C ++ 11?

(Причины, отличные от проблем совместимости с существующим кодом.)

user541686
источник
2
boost всегда на шаг впереди - «переносить типы дорого» ( boost.org/doc/libs/1_48_0/doc/html/container/move_emplace.html )
Щепурин
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) может не быть «инвариантным относительно местоположения», что означает, что адрес объекта является частью его значения. Например, ОС может хранить список указателей на все инициализированные объекты мьютекса. Если std::mutexв качестве члена данных содержится собственный тип мьютекса ОС, а адрес собственного типа должен оставаться фиксированным (поскольку ОС поддерживает список указателей на свои мьютексы), то любой из std::mutexних должен будет сохранить собственный тип мьютекса в куче, чтобы он оставался в одно и то же место при перемещении между std::mutexобъектами или объект std::mutexне должен перемещаться. Сохранение его в куче невозможно, потому что у a std::mutexесть constexprконструктор и он должен иметь право на постоянную инициализацию (т.е. статическую инициализацию), чтобы глобальныйstd::mutexгарантированно создается до начала выполнения программы, поэтому его конструктор не может использовать new. Так что остается единственный вариант - std::mutexоставаться неподвижным.

То же самое относится и к другим типам, которые содержат что-то, для чего требуется фиксированный адрес. Если адрес ресурса должен оставаться неизменным, не перемещайте его!

Есть еще один аргумент в пользу отказа от перемещения, std::mutexкоторый заключается в том, что было бы очень сложно сделать это безопасно, потому что вам нужно знать, что никто не пытается заблокировать мьютекс в момент его перемещения. Поскольку мьютексы являются одним из строительных блоков, которые вы можете использовать для предотвращения гонок данных, было бы прискорбно, если бы они были небезопасны против самих гонок! С неподвижным объектом std::mutexвы знаете, что единственное, что каждый может сделать с ним после того, как он был построен и до того, как он был уничтожен, - это заблокировать и разблокировать его, и эти операции явно гарантируются как потокобезопасные и не вызывают гонок данных. Тот же аргумент применяется к std::atomic<T>объектам: если они не могут быть перемещены атомарно, их невозможно будет безопасно перемещать, другой поток может пытаться вызватьcompare_exchange_strongна объекте прямо в момент его перемещения. Таким образом, другой случай, когда типы не должны быть подвижными, - это когда они являются низкоуровневыми строительными блоками безопасного параллельного кода и должны обеспечивать атомарность всех операций с ними. Если значение объекта может быть перемещено в новый объект в любое время, вам нужно будет использовать атомарную переменную для защиты каждой атомарной переменной, чтобы вы знали, безопасно ли ее использовать или она была перемещена ... и атомарную переменную для защиты эта атомарная переменная и так далее ...

Я думаю, что обобщил бы, сказав, что когда объект - это просто чистый фрагмент памяти, а не тип, который действует как держатель для значения или абстракции значения, нет смысла перемещать его. Основные типы, например, intне могут двигаться: их перемещение - это просто копия. Вы не можете вырвать кишки из a int, вы можете скопировать его значение, а затем установить его на ноль, но он все еще имеет intзначение, это просто байты памяти. Но intвсе еще подвиженв терминах языка, потому что копия является допустимой операцией перемещения. Однако для некопируемых типов, если вы не хотите или не можете перемещать фрагмент памяти, а также не можете скопировать его значение, он не может быть перемещен. Мьютекс или атомарная переменная - это определенное место в памяти (обрабатываемое особыми свойствами), поэтому не имеет смысла перемещать, а также не подлежит копированию, поэтому его нельзя перемещать.

Джонатан Уэйкли
источник
17
+1 менее экзотический пример чего-то, что нельзя переместить, потому что у него есть специальный адрес, - это узел в структуре ориентированного графа.
Potatoswatter
3
Если мьютекс нельзя копировать и нельзя перемещать, как я могу скопировать или переместить объект, содержащий мьютекс? (Как потокобезопасный класс с собственным мьютексом для синхронизации ...)
tr3w
4
@ tr3w, вы не можете, если вы не создадите мьютекс в куче и не удержите его с помощью unique_ptr или аналогичного
Джонатан Уэйкли
2
@ tr3w: Не могли бы вы просто переместить весь класс, кроме мьютекса?
user541686
3
@BenVoigt, но у нового объекта будет свой мьютекс. Я думаю, что он имеет в виду определяемые пользователем операции перемещения, которые перемещают все элементы, кроме члена мьютекса. Так что, если срок действия старого объекта истекает? Вместе с ним истекает его мьютекс.
Джонатан Уэйкли
57

Краткий ответ: если шрифт копируемый, он также должен быть перемещаемым. Однако обратное неверно: некоторые типы, например, std::unique_ptrможно перемещать, но копировать их не имеет смысла; это, естественно, типы только для передвижения.

Далее следует чуть более длинный ответ ...

Есть два основных типа типов (среди других, более специализированных, таких как черты характера):

  1. Типы, подобные значениям, например intили vector<widget>. Они представляют собой значения и, естественно, должны быть копируемыми. В C ++ 11, как правило, вы должны думать о перемещении как об оптимизации копирования, и поэтому все копируемые типы должны, естественно, быть перемещаемыми ... перемещение - это просто эффективный способ создания копии в часто распространенном случае, который вы не делаете. Мне больше не нужен исходный объект, и мы все равно собираемся его уничтожить.

  2. Типы, подобные ссылкам, которые существуют в иерархиях наследования, например базовые классы и классы с виртуальными или защищенными функциями-членами. Обычно они хранятся с помощью указателя или ссылки, часто с помощью base*или base&, и поэтому не обеспечивают создание копии, чтобы избежать разделения; если вы действительно хотите получить другой объект, такой как существующий, вы обычно вызываете виртуальную функцию, например clone. Они не нуждаются в построении или назначении перемещения по двум причинам: они не копируются, и в них уже есть еще более эффективная естественная операция «перемещения» - вы просто копируете / перемещаете указатель на объект, а сам объект не придется вообще переместиться в новое место в памяти.

Большинство типов попадает в одну из этих двух категорий, но есть и другие типы, которые также полезны, но реже. В частности, здесь типы, которые выражают уникальное владение ресурсом, например std::unique_ptr, естественно являются типами только для перемещения, потому что они не похожи на значения (нет смысла копировать их), но вы используете их напрямую (не всегда указателем или ссылкой) и поэтому хотите перемещать объекты этого типа из одного места в другое.

Херб Саттер
источник
61
Будет ли реальный Herb Sutter , пожалуйста , встать? :)
fredoverflow
6
Да, я перешел с одной учетной записи OAuth Google на другую, и мне не хотелось искать способ объединить две учетные записи, которые дает мне здесь. (Еще один аргумент против OAuth среди гораздо более убедительных.) Я, вероятно, больше не буду использовать другой, так что это то, что я буду использовать сейчас для периодической публикации SO.
Херб Саттер,
7
Я думал, что это std::mutexбыло неподвижно, поскольку мьютексы POSIX используются по адресу.
Puppy
9
@SChepurin: Вообще-то, это называется HerbOverflow.
sbi
26
Это набирает много голосов, никто не заметил, что там написано, когда тип должен быть доступен только для перемещения, вопрос не в этом? :)
Джонатан Уэйкли
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

billz
источник
1
... итераторы не должны быть подвижными ?! Что почему?
user541686
да, я думаю, это iterators / iterator adaptorsследует отредактировать, так как в C ++ 11 есть move_iterator?
billz
Хорошо, теперь я просто запутался. Вы говорите о итераторах , которые перемещают свои цели , или о перемещении итераторов самих ?
user541686
1
Так и есть std::reference_wrapper. Хорошо, остальные действительно кажутся неподвижными.
Christian Rau
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', который проверял бы, был ли перемещен охранник - это дополнительное if и влияние на производительность.

Да, конечно, его, вероятно, можно оптимизировать с помощью любого здравомыслящего оптимизатора, но все же приятно, что язык (для этого требуется C ++ 17, хотя, чтобы иметь возможность возвращать неподвижный тип, требует гарантированного копирования) не требует от нас заплатить, если мы все равно не собираемся перемещать охранник, кроме как вернуть его из функции создания (принцип «не плати за то, что ты не используешь»).

saarraz1
источник