Реализация копирования и перемещения назначения с помощью одной функции

Как правило, учитывая некоторый тип T, чтобы реализовать копирование и перемещение задания, нужны две функции

T& operator=(T&&) { ... }
T& operator=(const T&) { ... }

Недавно я пришел к выводу, что одного достаточно

T& operator=(T v) {
swap(v);
return *this;
}

Эта версия использует конструктор копирования / перемещения. Является ли задание копированием или перемещением, зависит от того, как v построен Эта версия может быть даже быстрее, чем первая, так как передача по значению дает больше места для оптимизации компилятора [1]. Итак, в чем преимущество первой версии перед второй, когда ее использует даже стандартная библиотека?

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

6

Решение

std::swap реализуется путем выполнения построения перемещения, за которым следуют две операции назначения перемещения. Так что, если вы не реализуете свой собственный swap операция, которая заменяет стандартную, ваш код в представленном виде представляет собой бесконечный цикл.

Таким образом, вы можете реализовать 2 operator= методы или реализовать один operator= метод и один swap метод. С точки зрения количества вызываемых функций, оно в конечном итоге идентично.

Кроме того, ваша версия operator= иногда менее эффективен. Если конструкция параметра не исключена, эта конструкция будет выполняться путем копирования / перемещения из значения вызывающей стороны. После этого 1 конструкция хода и 2 назначения хода (или что угодно swap делает). Тогда как собственно operator= Перегрузки могут работать напрямую с указанием ссылки.

И это предполагает, что вы не можете написать оптимальную версию фактического назначения. Рассмотреть возможность копирования vector другому. Если пункт назначения vector имеет достаточно места для хранения размера исходного вектора … вам не нужно выделять. Принимая во внимание, что если вы копируете сооружать, Вы должны выделить место для хранения. Только чтобы потом освободить хранилище, которое вы могли бы использовать.

Даже в лучшем случае ваша копия / перемещение&свопа не будет Больше эффективнее, чем использование значения. В конце концов, вы собираетесь взять ссылку на параметр; std::swap не работает на ценностях. Так что, как вы думаете, эффективность при использовании ссылок будет потеряна в любом случае.

Принципиальные аргументы в пользу копирования / перемещения&поменять местами являются:

  1. Сокращение дублирования кода. Это выгодно только в том случае, если ваша реализация операций назначения копирования / перемещения будет более или менее идентична конструкции копирования / перемещения. Это не верно для многих типов; как говорилось ранее, vector может немного оптимизировать себя, используя существующее хранилище, где это возможно. Действительно, многие контейнеры могут (особенно контейнеры последовательности).

  2. Предоставление сильной гарантии исключения с минимальными усилиями. Предполагая, что ваш конструктор перемещения не исключение.

Лично я предпочитаю вообще избегать сценария. Я предпочитаю позволить компилятору генерировать все мои специальные функции-члены. И если типу абсолютно необходимо, чтобы я написал эти специальные функции-члены, тогда этот тип будет минимально возможным. То есть единственной целью будет управление всем, что требует этой операции.

Таким образом, мне просто не нужно об этом беспокоиться. Львиная доля моих классов не нуждается в явном определении этих функций.

4

Другие решения

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

  1. Унифицированный оператор присваивания. Это означает, что у вас есть один оператор присваивания, который принимает значение вместо двух перегрузок, которые принимают const & а также &&,
  2. Оператор присваивания копии и свопа (CAS).

Если вы делаете 1, вы обычно делаете 2. Поскольку вам нужно где-то реализовать логику присваивания swap / move, и вы не можете реализовать ее в унифицированном операторе присваивания, поэтому обычно вы реализуете swap и вызываете ее. Но выполнение 2 не означает, что вы должны сделать 1:

T& operator=(T&&) { /* actually implemented */ }
T& operator=(const T& t) { T t2(t); swap(*this, t2); return *this;}

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

Мотивация для выполнения CAS состоит в том, чтобы получить гарантию сильного исключения, хотя как T.C. указывает в комментариях, вы можете сделать:

T& operator=(const T& t) { *this = T(t); return *this;}

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

Вы должны сделать 1 никогда. Желательно, чтобы они были отдельными функциями, чтобы назначение перемещения могло быть помечено как исключение.

1

По вопросам рекламы [email protected]