Делать обмен быстрее, проще в использовании и безопасным для исключений - PullRequest
14 голосов
/ 02 февраля 2011

Я не мог спать прошлой ночью и начал думать о std::swap.Вот знакомая версия C ++ 98:

template <typename T>
void swap(T& a, T& b)
{
    T c(a);
    a = b;
    b = c;
}

Если пользовательский класс Foo использует внешние ресурсы, это неэффективно.Обычная идиома - предоставить метод void Foo::swap(Foo& other) и специализацию std::swap<Foo>.Обратите внимание, что это не работает с классами templates , поскольку вы не можете частично специализировать шаблон функции, а перегрузка имен в пространстве имен std недопустима.Решение состоит в том, чтобы написать шаблонную функцию в собственном пространстве имен и полагаться на поиск в зависимости от аргумента, чтобы найти его.Это критически зависит от клиента, который будет следовать идиоме using std::swap вместо прямого вызова std::swap.Очень хрупкий.

В C ++ 0x, если Foo имеет определяемый пользователем конструктор перемещения и оператор присваивания перемещения, предоставляя пользовательский метод swap и специализация std::swap<Foo> практически не имеет производительностиПреимущество, потому что версия C ++ 0x std::swap использует эффективные ходы вместо копий:

#include <utility>

template <typename T>
void swap(T& a, T& b)
{
    T c(std::move(a));
    a = std::move(b);
    b = std::move(c);
}

Больше не нужно возиться с swap, и это уже отнимает у программиста большую нагрузку.Текущие компиляторы еще не генерируют конструкторы перемещения и операторы присваивания перемещения автоматически, но, насколько я знаю, это изменится.Единственная оставшаяся проблема - безопасность исключений, потому что в общем случае операции перемещения разрешены, и это открывает целую банку червей.Вопрос "Каково состояние перемещенного объекта?"еще более усложняет ситуацию.

Тогда я подумал, какова семантика std::swap в C ++ 0x, если все идет хорошо?Каково состояние объектов до и после обмена?Как правило, замена с помощью операций перемещения не затрагивает внешние ресурсы, а только сами «плоские» представления объектов.

Так почему бы просто не написать шаблон swap, который выполняет именно это: меняет местами представления объектов ?

#include <cstring>

template <typename T>
void swap(T& a, T& b)
{
    unsigned char c[sizeof(T)];

    memcpy( c, &a, sizeof(T));
    memcpy(&a, &b, sizeof(T));
    memcpy(&b,  c, sizeof(T));
}

Это так же эффективно, как и получается: просто гремит через сырую память.Это не требует какого-либо вмешательства со стороны пользователя: не нужно определять никаких специальных методов обмена или операций перемещения.Это означает, что он работает даже в C ++ 98 (который не имеет ссылок на значения), заметьте).Но что еще более важно, теперь мы можем забыть о проблемах безопасности исключений , потому что memcpy никогда не выбрасывает.

Я вижу две потенциальные проблемы с этим подходом:

* 1040Во-первых, не все объекты предназначены для обмена.Если конструктор класса скрывает конструктор копирования или оператор присваивания копии, попытка поменять объекты класса не удастся во время компиляции.Мы можем просто ввести некоторый мертвый код, который проверяет, допустимо ли копирование и присваивание для типа:
template <typename T>
void swap(T& a, T& b)
{
    if (false)    // dead code, never executed
    {
        T c(a);   // copy-constructible?
        a = b;    // assignable?
    }

    unsigned char c[sizeof(T)];

    std::memcpy( c, &a, sizeof(T));
    std::memcpy(&a, &b, sizeof(T));
    std::memcpy(&b,  c, sizeof(T));
}

Любой приличный компилятор может легко избавиться от мертвого кода.(Возможно, есть более эффективные способы проверки «соответствия свопа», но это не главное. Важно то, что это возможно).

Во-вторых, некоторые типы могут выполнять «необычные» действия в конструкторе копирования иоператор копирования копирования.Например, они могут уведомить наблюдателей об их изменении.Я считаю это незначительной проблемой, потому что такие типы объектов, вероятно, не должны были обеспечивать операции копирования.

Пожалуйста, дайте мне знать, что вы думаете об этом подходе к обмену.Будет ли это работать на практике?Вы бы использовали это?Можете ли вы определить типы библиотек, где это может сломаться?Видите ли вы дополнительные проблемы?Обсудить!

Ответы [ 5 ]

20 голосов
/ 02 февраля 2011

Так почему бы просто не написать шаблон swap, который делает именно это: swap представления объектов *?

Есть много способов, которыми объект, однажды построенный, может сломаться, когда вы копируете байты, в которых он находится. На самом деле, можно придумать, казалось бы, бесконечное количество случаев , где это не будет поступайте правильно - хотя на практике это может работать в 98% всех случаев.

Это потому, что основная проблема для всего этого заключается в том, что, кроме C, в C ++ мы не должны обрабатывать объекты, как если бы они были просто необработанными байтами . Вот почему у нас есть строительство и разрушение: превратить хранилище в объекты и объекты обратно в хранилище. После запуска конструктора память, в которой находится объект, становится больше, чем просто необработанное хранилище. Если вы относитесь к нему так, как будто это не так, вы сломаете некоторые типы.

Однако, по сути, движущиеся объекты не должны работать намного хуже, чем ваша идея, потому что, как только вы начинаете рекурсивно встраивать вызовы в std::move(), вы, как правило, в конечном итоге достигаете того, где встроенных перемещены . (И если для некоторых типов есть что-то еще, лучше не тратить время на память о них!) Конечно, перемещение памяти в блоке обычно быстрее, чем отдельные перемещения (и маловероятно, что компилятор обнаружит, что может оптимизировать отдельные движения к одному всеобъемлющему std::memcpy()), но это цена, которую мы платим за абстракцию, которую нам предлагают непрозрачные объекты. И он довольно маленький, особенно если сравнить его с копированием, которое мы делали раньше.

Однако вы можете оптимизировать swap(), используя std::memcpy() для агрегатных типов .

20 голосов
/ 02 февраля 2011

Это сломает экземпляры классов, которые имеют указатели на своих собственных членов.Например:

class SomeClassWithBuffer {
  private:
    enum {
      BUFSIZE = 4096,
    };
    char buffer[BUFSIZE];
    char *currentPos; // meant to point to the current position in the buffer
  public:
    SomeClassWithBuffer();
    SomeClassWithBuffer(const SomeClassWithBuffer &that);
};

SomeClassWithBuffer::SomeClassWithBuffer():
  currentPos(buffer)
{
}

SomeClassWithBuffer::SomeClassWithBuffer(const SomeClassWithBuffer &that)
{
  memcpy(buffer, that.buffer, BUFSIZE);
  currentPos = buffer + (that.currentPos - that.buffer);
}

Теперь, если вы просто выполните memcpy (), где будет указывать currentPos?На старое место, очевидно.Это приведет к очень забавным ошибкам, когда каждый экземпляр фактически использует буфер другого.

7 голосов
/ 02 февраля 2011

Некоторые типы можно менять местами, но нельзя копировать.Уникальные умные указатели, вероятно, лучший пример.Проверка на копируемость и назначаемость неверна.

Если T не является типом POD, использование memcpy для копирования / перемещения является неопределенным поведением.


Общая идиомапредоставить метод void Foo :: swap (Foo & other) и специализацию std :: swap .Обратите внимание, что это не работает с шаблонами классов,…

Лучшей идиомой является своп, не связанный с членством, и требующий от пользователей называть своп безусловным, поэтому применяется ADL.Это также работает с шаблонами:

struct NonTemplate {};
void swap(NonTemplate&, NonTemplate&);

template<class T>
struct Template {
  friend void swap(Template &a, Template &b) {
    using std::swap;
#define S(N) swap(a.N, b.N);
    S(each)
    S(data)
    S(member)
#undef S
  }
};

Ключом является объявление использования для std :: swap в качестве запасного варианта.Дружба для обмена шаблонами хороша для упрощения определения;своп для NonTemplate также может быть другом, но это деталь реализации.

6 голосов
/ 02 февраля 2011

Я считаю, что это незначительная проблема, потому что такого рода объекты, вероятно, не должны были обеспечивать операции копирования.

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

, поскольку вВообще, операции перемещения разрешены для броска

Они, безусловно, нет.Практически невозможно гарантировать безопасность сильных исключений практически при любых обстоятельствах, связанных с ходами, когда ход может быть сгенерирован.Определение стандартной библиотеки C ++ 0x из памяти прямо гласит, что любой тип, используемый в любом стандартном контейнере, не должен выбрасывать при перемещении.

Это так же эффективно, как и получить

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

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

1 голос
/ 02 февраля 2011

ваша swap версия вызовет хаос, если кто-то использует ее с полиморфными типами.

учтите:

Base *b_ptr = new Base();    // Base and Derived contain definitions
Base *d_ptr = new Derived(); // of a virtual function called vfunc()
yourmemcpyswap( *b_ptr, *d_ptr );
b_ptr->vfunc(); //now calls Derived::vfunc, while it should call Base::vfunc
d_ptr->vfunc(); //now calls Base::vfunc while it should call Derived::vfunc
//...

это неправильно, потому что теперь b содержит vtable таблицы Derived type, поэтому Derived::vfunc вызывается для объекта, который не относится к типу Derived.

Нормальный std::swap меняет местами только элементы данных Base, так что все в порядке с std::swap

...