Быстрое использование swap, удобство использования и исключение
Я не мог спать прошлой ночью и начал думать о std::swap
. Вот знакомая версия С++ 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>
. Обратите внимание, что это не работает с шаблонами классов, поскольку вы не можете частично специализировать шаблон функции, а перегрузка имен в пространстве имен std
является незаконной. Решение состоит в том, чтобы написать функцию шаблона в одном собственном пространстве имен и полагаться на зависящий от аргумента поиск, чтобы найти его. Это критически зависит от того, что клиент будет следовать "using std::swap
idiom" вместо прямого вызова std::swap
. Очень хрупкий.
В С++ 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
в С++ 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));
}
Это так же эффективно, как и получается: он просто взрывается из-за необработанной памяти. Он не требует вмешательства пользователя: никаких специальных методов свопа или операций перемещения не требуется. Это означает, что он даже работает на С++ 98 (у которого нет ссылок на rvalue, заметьте). Но что еще более важно, мы можем теперь забыть об исключениях, связанных с безопасностью, потому что memcpy
никогда не бросает.
Я вижу две потенциальные проблемы с этим подходом:
Во-первых, не все объекты должны быть заменены. Если конструктор классов скрывает конструктор копирования или оператор присваивания копии, попытка смены объектов класса должна завершиться неудачей во время компиляции. Мы можем просто ввести некоторый мертвый код, который проверяет, являются ли копирование и присвоение законными по типу:
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));
}
Любой достойный компилятор может тривиально избавиться от мертвого кода. (Вероятно, есть более эффективные способы проверки "соответствия подкачки", но это не главное. Важно то, что это возможно).
Во-вторых, некоторые типы могут выполнять "необычные" действия в конструкторе копирования и операторе присваивания копии. Например, они могут уведомить наблюдателей об их изменении. Я считаю это второстепенной проблемой, потому что такие объекты, вероятно, не должны были предоставлять операции копирования в первую очередь.
Пожалуйста, дайте мне знать, что вы думаете об этом подходе к обмену. Будет ли это работать на практике? Вы бы использовали его? Можете ли вы определить типы библиотек, где это сломается? Вы видите дополнительные проблемы? Обсудить!
Ответы
Ответ 1
Итак, почему бы просто не написать шаблон swap
, который делает именно это: обменивать представления объектов *?
Существует множество способов, с помощью которых объект, будучи сконструированным, может сломаться, когда вы копируете байты, в которых он находится. Фактически, можно было бы найти бесконечное количество случаев , где это не сделало бы прав вещь - хотя на практике это может работать в 98% всех случаев.
Это потому, что основная проблема для всего этого заключается в том, что, кроме C, в С++ мы не должны обрабатывать объекты, как если бы они были просто необработанными байтами. Вот почему у нас есть конструкция и разрушение, в конце концов: превратить необработанное хранилище в объекты и объекты обратно в сырое хранилище. После запуска конструктора память, в которой находится объект, представляет собой не только сырое хранилище. Если вы относитесь к нему так, как будто это не так, вы разбиваете некоторые типы.
Однако, по существу, движущиеся объекты не должны выполнять намного хуже, чем ваша идея, потому что, как только вы начнете рекурсивно встраивать вызовы в std::move()
, вы обычно в конечном итоге достигаете места, где перемещаются встроенные элементы. (И если вам нужно перемещаться по некоторым типам, вам лучше не поиграть с их памятью!) Конечно, перемещение памяти в блоке обычно происходит быстрее, чем одиночные ходы (и маловероятно, что компилятор может узнать, что он может оптимизировать отдельные движения до одного всеобъемлющего std::memcpy()
), но мы предлагаем цену, которую мы платим за абстракцию непрозрачных объектов. И это довольно мало, особенно когда вы сравниваете его с копированием, которое мы использовали.
Однако вы могли бы оптимизировать swap()
, используя std::memcpy()
для агрегатных типов.
Ответ 2
Это приведет к слому экземпляров класса, которые имеют указатели на собственные члены. Например:
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(), где будет текущая точка? Очевидно, что в старом месте. Это приведет к очень забавным ошибкам, когда каждый экземпляр фактически использует другой буфер.
Ответ 3
Некоторые типы могут быть заменены, но не могут быть скопированы. Лучшим примером могут служить уникальные интеллектуальные указатели. Проверка на возможность копирования и назначения невозможна.
Если T не является типом POD, использование memcpy для копирования/перемещения - это поведение undefined.
Общая идиома заключается в предоставлении метода void Foo:: swap (Foo & other) и специализации std:: swap <Foo> . Обратите внимание, что это не работает с шаблонами классов,...
Лучшая идиома - это не-членный своп и требует, чтобы пользователи вызывали swap unqualified, поэтому применяется 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 в качестве резервной копии. Дружба для обмена шаблонами хороша для упрощения определения; swap для NonTemplate также может быть другом, но это деталь реализации.
Ответ 4
Я считаю это незначительной проблемой, потому что такие объекты, вероятно, должны не предоставили операции копирования в на первом месте.
То есть, просто, нагрузка неправильная. Классы, которые уведомляют наблюдателей и классы, которые не должны копироваться, полностью не связаны. Как насчет shared_ptr? Очевидно, он должен быть скопирован, но он также явно уведомляет наблюдателя - счетчик ссылок. Теперь верно, что в этом случае подсчет ссылок одинаковый после swap, но это определенно неверно для всех типов, и это особенно не верно, если задействована многопоточность, это неверно в случае обычной копии вместо обмен и т.д. Это особенно неправильно для классов, которые могут быть перемещены или заменены, но не скопированы.
потому что в целом операции перемещения разрешено бросать
Они, безусловно, нет. Практически невозможно гарантировать сильную безопасность исключений в почти любых обстоятельствах, связанных с ходами, когда движение может бросить. В С++ 0x определение стандартной библиотеки из памяти явно указывается, что любой тип, который можно использовать в любом стандартном контейнере, не должен бросаться при перемещении.
Это так же эффективно, как и
Это тоже неправильно. Вы предполагаете, что перемещение любого объекта является чисто переменными-членами, но это может быть не все из них. У меня может быть кеш на основе реализации, и я могу решить, что в моем классе я не должен перемещать этот кеш. В качестве детализации реализации полностью в моих правах не перемещать какие-либо переменные-члены, которые, по моему мнению, не нужны для перемещения. Вы, однако, хотите переместить все из них.
Теперь верно, что ваш примерный код должен быть действительным для многих классов. Тем не менее, он чрезвычайно определенно недействителен для многих классов, которые полностью и полностью легитимны, и, что более важно, он собирается скомпилировать эту операцию в любом случае, если операция может быть сведена до этого. Это потрясающие совершенно хорошие классы для абсолютно никакой пользы.
Ответ 5
ваша версия 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
, поэтому Derived::vfunc
вызывается для объекта, который не имеет тип Derived
.
Обычный std::swap
заменяет только члены данных Base
, поэтому это нормально с std::swap