Я не мог спать прошлой ночью и начал думать о 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));
}
Любой достойный компилятор может тривиально избавиться от мертвого кода. (Вероятно, есть более эффективные способы проверки "соответствия подкачки", но это не главное. Важно то, что это возможно).
Во-вторых, некоторые типы могут выполнять "необычные" действия в конструкторе копирования и операторе присваивания копии. Например, они могут уведомить наблюдателей об их изменении. Я считаю это второстепенной проблемой, потому что такие объекты, вероятно, не должны были предоставлять операции копирования в первую очередь.
Пожалуйста, дайте мне знать, что вы думаете об этом подходе к обмену. Будет ли это работать на практике? Вы бы использовали его? Можете ли вы определить типы библиотек, где это сломается? Вы видите дополнительные проблемы? Обсудить!