Почему есть семантика перемещения?

Позвольте мне предисловие, сказав, что я прочитал некоторые из многих уже заданных вопросов относительно семантики перемещения. Этот вопрос заключается не в том, как использовать семантику перемещения, она спрашивает, какова цель этого - если я не ошибаюсь, я не понимаю, зачем нужна семантика.

Фон

Я выполнял тяжелый класс, который для целей этого вопроса выглядел примерно так:

class B;

class A
{
private:
    std::array<B, 1000> b;
public:
    // ...
}

Когда пришло время сделать оператор присваивания перемещения, я понял, что могу значительно оптимизировать процесс, изменив член b на std::array<B, 1000> *b; - тогда перемещение может быть просто удалением и указателем swap.

Это привело меня к следующей мысли: теперь не должны все не примитивные члены типа быть указателями для ускорения движения (исправлено ниже [1] [2]) (есть случай для случаев, когда память не должна динамически распределяться, но в этих случаях оптимизация движения не является проблемой, поскольку нет способа сделать это)?

Вот где у меня была следующая реализация: зачем создавать класс A, который на самом деле просто содержит указатель b, поэтому замена позже проще, когда я могу просто сделать указатель на весь класс A. Очевидно, что если клиент ожидает, что движение будет значительно быстрее, чем копирование, клиент должен быть в порядке с распределением динамической памяти. Но в этом случае, почему клиент не просто динамически выделяет весь класс A?

Вопрос

Не может ли клиент уже использовать указатели, чтобы сделать все, что переносит семантику? Если да, то в чем смысл семантики перемещения?

Переместить семантику:

std::string f()
{
    std::string s("some long string");
    return s;
}

int main()
{
    // super-fast pointer swap!
    std::string a = f();
    return 0;
}

Указатели

std::string *f()
{
    std::string *s = new std::string("some long string");
    return s;
}

int main()
{
    // still super-fast pointer swap!
    std::string *a = f();
    delete a;
    return 0;
}

И здесь сильное задание, которое все говорят, настолько велико:

template<typename T>
T& strong_assign(T *&t1, T *&t2)
{
    delete t1;
    // super-fast pointer swap!
    t1 = t2;
    t2 = nullptr;
    return *t1;
}

#define rvalue_strong_assign(a, b) (auto ___##b = b, strong_assign(a, &___##b))

Изобразительное - последнее в обоих примерах может считаться "плохим стилем" - что бы это ни значило - но действительно ли это стоит всех проблем с двойными амперсандами? Если исключение может быть выброшено до вызова delete a, это все еще не является реальной проблемой - просто сделайте охрану или используйте unique_ptr.

Изменить [1] Я просто понял, что это не будет необходимо для классов, таких как std::vector, которые сами используют распределение динамической памяти и имеют эффективные методы перемещения. Это просто умаляет мысль, которую я имел - вопрос ниже все еще стоит.

Изменить [2] Как упоминалось в обсуждении в комментариях и ответах ниже, весь этот вопрос довольно спорный. Следует использовать семантику значения как можно больше, чтобы избежать накладных расходов на распределение, поскольку клиент всегда может переместить все это в кучу, если это необходимо.

Ответы

Ответ 1

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

Они также позволяют вам более эффективно представлять не скопируемые ресурсы, такие как сокеты.

Перемещение семантики не является строго необходимым, так как вы можете видеть, что С++ существует для 40 лет a без них. Это просто лучший способ представить определенные понятия и оптимизацию.

Ответ 2

Я полностью наслаждался всеми ответами и комментариями! И я согласен со всеми из них. Я просто хотел придерживаться еще одной мотивации, о которой никто еще не упомянул. Это происходит от N1377:

Перемещение семантики в основном связано с оптимизацией производительности: способность для перемещения дорогостоящего объекта с одного адреса в память на другой, при использовании ресурсов источника, чтобы построить с минимальными затратами.

Перемещение семантики уже существует в текущем языке и библиотеке в определенной степени:

  • копирование конструктора в некоторых контекстах
  • auto_ptr "copy"
  • список:: сращивания
  • замена контейнеров

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

т.е. в общем коде, таком как vector::erase, нужен один унифицированный синтаксис для значений move, чтобы подключить отверстие, оставшееся от стираемого значения. Нельзя использовать swap, потому что это было бы слишком дорого, если value_type - int. И нельзя использовать назначение копирования, поскольку это было бы слишком дорого, если value_type - A (OP A). Ну, можно было бы использовать назначение копирования, ведь мы все сделали в С++ 98/03, но это смешно дорого.

не все члены непримитивного типа являются указателями для ускорения движения.

Это было бы ужасно дорого, если тип элемента complex<double>. Может также цвет его Java.

Ответ 3

Не может ли клиент уже использовать указатели, чтобы сделать все, что переносит семантику? Если да, то в чем смысл семантики перемещения?

Ваш второй пример дает одну очень вескую причину, почему перенос семантики - это хорошо:

std::string *f()
{
    std::string *s = new std::string("some long string");
    return s;
}

int main()
{
    // still super-fast pointer swap!
    std::string *a = f();
    delete a;
    return 0;
}

Здесь клиент должен проверить реализацию, чтобы выяснить, кто несет ответственность за удаление указателя. С семантикой перемещения эта проблема с владельцем даже не появится.

Если исключение может быть выбрано до вызова delete a, это еще не проблема, просто сделайте охрану или используйте unique_ptr.

Опять же, проблема с уродливым владельцем появляется, если вы не используете семантику перемещения. Кстати, как вы бы внедрили unique_ptr без семантики перемещения?

Я знаю о auto_ptr, и есть веские причины, по которым он теперь устарел.

Действительно ли это стоит всех проблем с двойными амперсандами?

Правда, для того, чтобы привыкнуть к нему, требуется некоторое время. После того, как вы знакомы с ним, вам будет интересно, как вы можете жить без семантики перемещения.

Ответ 4

Ваш пример строки отличный. Оптимизация коротких строк означает, что короткие std::string не существуют в свободном хранилище: вместо этого они существуют в автоматическом хранилище.

Версия new/delete означает, что вы вставляете каждый std::string в свободный магазин. Версия move помещает большие строки в свободное хранилище, а маленькие строки остаются (и, возможно, копируются) в автоматическом хранилище.

Кроме того, в вашей версии указателя отсутствует безопасность исключений, так как он имеет обработчики ресурсов, отличных от RAII. Даже если вы не используете исключения, владельцы ресурса голого указателя в основном вынуждают поток контроля точки выхода для управления очисткой. Кроме того, использование голой собственности указателя приводит к утечкам ресурсов и оборванным указателям.

Таким образом, версия голого указателя хуже в грудах способов.

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

Указатели помещают служебные данные управления ресурсами в клиенты, в то время как хорошие классы С++ 11 обрабатывают эту проблему для вас. move семантика делает это и более легким в обслуживании, и гораздо меньше подвержено ошибкам.