Почему есть семантика перемещения?
Позвольте мне предисловие, сказав, что я прочитал некоторые из многих уже заданных вопросов относительно семантики перемещения. Этот вопрос заключается не в том, как использовать семантику перемещения, она спрашивает, какова цель этого - если я не ошибаюсь, я не понимаю, зачем нужна семантика.
Фон
Я выполнял тяжелый класс, который для целей этого вопроса выглядел примерно так:
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
семантика делает это и более легким в обслуживании, и гораздо меньше подвержено ошибкам.