Когда нужно сделать тип, не перемещаемый в С++ 11?
Я был удивлен, что это не появилось в моих результатах поиска, я думал, что кто-то спросил об этом раньше, учитывая полезность семантики перемещения в С++ 11:
Когда мне нужно (или это хорошая идея для меня) сделать класс не движимым в С++ 11?
(Причины, кроме проблем с совместимостью с существующим кодом.)
Ответы
Ответ 1
Ответ Herb (до того, как он был отредактирован) действительно дал хороший пример типа, который не должен быть подвижным: std::mutex
.
Собственный тип мьютекса OS (например, pthread_mutex_t
на платформах POSIX) может не быть "инвариантом местоположения", то есть адрес объекта является частью его значения. Например, ОС может содержать список указателей ко всем инициализированным объектам мьютекса. Если std::mutex
содержит родной тип mutex OS в качестве члена данных, а адрес родного типа должен оставаться фиксированным (поскольку ОС поддерживает список указателей на его мьютексы), то либо 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
по-прежнему перемещается в языковых терминах, потому что копия является действительной операцией перемещения. Однако для не копируемых типов, если вы не хотите или не можете перемещать кусок памяти, и вы также не можете скопировать его значение, то оно не движется. Мьютекс или атомная переменная - это конкретное местоположение памяти (обработанное специальными свойствами), поэтому не имеет смысла перемещаться, а также не может быть скопировано, поэтому оно не движется.
Ответ 2
Короткий ответ: если тип скопирован, он также должен быть подвижным. Однако обратное неверно: некоторые типы, такие как std::unique_ptr
, являются подвижными, но нет смысла их копировать; это, естественно, типы только для перемещения.
Слегка длинный ответ следует...
Существует два основных типа типов (среди других более специальных целей, таких как черты):
-
Сопоставимые типы, такие как int
или vector<widget>
. Они представляют значения и, естественно, должны быть скопируемыми. В С++ 11, как правило, вы должны думать о том, чтобы двигаться как оптимизация копии, и поэтому все типы, подлежащие копированию, должны, естественно, быть подвижными... перемещение - это просто эффективный способ сделать копию в часто встречающемся случае, t нужен оригинальный объект, и он все равно собирается его уничтожить.
-
Вспомогательные типы, существующие в иерархиях наследования, такие как базовые классы и классы с виртуальными или защищенными функциями-членами. Обычно они удерживаются указателем или ссылкой, часто base*
или base&
, и поэтому не предусматривают построение копирования, чтобы избежать нарезки; если вы хотите получить еще один объект, как существующий, вы обычно называете виртуальную функцию, например clone
. Им не нужно перемещать конструкцию или назначение по двум причинам: они не копируются, и у них уже есть более эффективная естественная операция "переместить" - вы просто копируете/перемещаете указатель на объект, а сам объект не должны вообще перемещаться в новое место памяти.
Большинство типов попадают в одну из этих двух категорий, но есть и другие типы типов, которые также полезны, просто реже. В частности, типы, которые выражают уникальное право собственности на ресурс, например std::unique_ptr
, являются, естественно, типами только для перемещения, поскольку они не являются ценностными (их нет смысла копировать), но вы используете их напрямую (не всегда по указателю или ссылке) и поэтому хотите перемещать объекты этого типа из одного места в другое.
Ответ 3
На самом деле, когда я просматриваю, я обнаружил, что некоторые типы в С++ 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
По-видимому, есть дискуссия о Кланге: https://groups.google.com/forum/?fromgroups=#!topic/comp.std.c++/pCO1Qqb3Xa4
Ответ 4
Еще одна причина, по которой я нашел - производительность.
Скажем, у вас есть класс 'a', который имеет значение.
Вы хотите вывести интерфейс, который позволяет пользователю изменять значение в течение ограниченного времени (для области).
Способ достижения этого заключается в возвращении объекта 'scope guard' из '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);
}
}
Если я сделал mov_value_guard подвижным, мне пришлось бы добавить "if" к его деструктору, который будет проверять, был ли защищен перемещен, - это дополнительный, если и влияние производительности.
Да, конечно, его можно оптимизировать любым разумным оптимизатором, но все же приятно, что язык (для этого требуется С++ 17, чтобы иметь возможность возвращать недвижущийся тип, требует гарантированного копирования) не требовать от нас оплаты, если, если мы не собираемся переместить охрану в любом случае, кроме как вернуть ее из функции создания (принцип dont-pay-for-what-you-dont-use).