Когда сделать тип неподвижным в 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 подвижны, но копировать их не имеет смысла; это, естественно, только для перемещения типов.

Чуть более длинный ответ следует...

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

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

  2. Подобные ссылкам типы, которые существуют в иерархиях наследования, такие как базовые классы и классы с виртуальными или защищенными функциями-членами. Они обычно хранятся с помощью указателя или ссылки, часто 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).

Другие вопросы по тегам