Когда сделать тип неподвижным в C++11?
Я был удивлен, что это не показывается в моих результатах поиска, я думал, что кто-то бы спрашивал это раньше, учитывая полезность семантики перемещения в C++ 11:
Когда мне нужно (или это хорошая идея для меня) сделать класс неподвижным в C++ 11?
(Причины, кроме проблем совместимости с существующим кодом, то есть.)
4 ответа
Ответ Херба (до того, как он был отредактирован) фактически дал хороший пример типа, который не должен быть подвижным: std::mutex
,
Собственный тип мьютекса ОС (например, pthread_mutex_t
на платформах POSIX) может не быть "инвариантом местоположения", то есть адрес объекта является частью его значения. Например, ОС может хранить список указателей на все инициализированные объекты мьютекса. Если 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
все еще подвижен в языковых терминах, потому что копия является допустимой операцией перемещения. Однако для не копируемых типов, если вы не хотите или не можете переместить часть памяти, и вы также не можете скопировать ее значение, то она не может быть перемещена. Мьютекс или атомарная переменная - это определенное место в памяти (обработанное специальными свойствами), поэтому не имеет смысла перемещаться, а также не копируется, поэтому не может быть перемещено.
Краткий ответ: если тип копируемый, он также должен быть перемещаемым. Однако обратное неверно: некоторые типы, такие как std::unique_ptr
подвижны, но копировать их не имеет смысла; это, естественно, только для перемещения типов.
Чуть более длинный ответ следует...
Есть два основных типа типов (среди других более специализированных, таких как черты):
Типы типа значения, такие как
int
или жеvector<widget>
, Они представляют собой значения и, естественно, должны быть копируемыми. В C++11, как правило, вы должны думать о перемещении как об оптимизации копии, и поэтому все копируемые типы, естественно, должны быть перемещаемыми... перемещение - это всего лишь эффективный способ сделать копию в часто встречающемся случае, который вы не делаете. Тебе больше не нужен оригинальный объект, и он все равно собирается его уничтожить.Подобные ссылкам типы, которые существуют в иерархиях наследования, такие как базовые классы и классы с виртуальными или защищенными функциями-членами. Они обычно хранятся с помощью указателя или ссылки, часто
base*
или жеbase&
и так не предоставляют конструкцию копирования, чтобы избежать нарезки; если вы хотите получить другой объект, такой же, как существующий, вы обычно вызываете виртуальную функциюclone
, Они не нуждаются в построении перемещения или назначении по двум причинам: они не копируются, и у них уже есть еще более эффективная естественная операция перемещения - вы просто копируете / перемещаете указатель на объект, а сам объект не делает придется переместиться в новую ячейку памяти вообще.
Большинство типов попадают в одну из этих двух категорий, но есть и другие типы типов, которые также полезны, но встречаются реже. В частности, здесь типы, которые выражают уникальное право собственности на ресурс, такие как std::unique_ptr
, являются, естественно, только перемещаемыми типами, потому что они не имеют значения (не имеет смысла копировать их), но вы используете их напрямую (не всегда по указателю или по ссылке) и поэтому хотите перемещать объекты этого типа вокруг из одного места в другое.
На самом деле, когда я искал, я обнаружил, что некоторые типы в 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=
Еще одна причина, которую я нашел - производительность. Скажем, у вас есть класс "а", который содержит значение. Вы хотите вывести интерфейс, который позволяет пользователю изменять значение в течение ограниченного времени (для области действия).
Один из способов добиться этого - вернуть объект '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);
}
}
Если бы я сделал change_value_guard перемещаемым, мне пришлось бы добавить 'if' к его деструктору, который бы проверял, был ли удален охранник - это дополнительное if и влияние на производительность.
Да, конечно, он может быть оптимизирован любым здравомыслящим оптимизатором, но все-таки приятно, что язык (для этого требуется C++17, хотя для того, чтобы иметь возможность возвращать неподвижный тип требует гарантированного разрешения копирования), нам не требуется заплатить это, если мы не собираемся перемещать охрану в любом случае, кроме как вернуть его из функции создания (принцип dont-pay-for-what-you-dont-use).