Когда нужно сделать тип, не перемещаемый в С++ 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).