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

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

Фон

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

class B;

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

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

Это привело меня к следующей мысли: теперь, не должны ли все элементы не примитивного типа быть указателями для ускорения движения (исправлено ниже [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,

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

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

4 ответа

Решение

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

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

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

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

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

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

  • скопировать конструктор elision в некоторых контекстах
  • auto_ptr "копия"
  • Список:: сращивания
  • своп на контейнерах

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

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

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

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

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

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

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 и есть веские причины, почему он сейчас устарел.

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

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

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

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

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

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

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

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

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