Повторное использование идиомы копирования и свопинга

Я пытаюсь поместить идиому copy-and-swap в многоразовый mixin:

template<typename Derived>
struct copy_and_swap
{
    Derived& operator=(Derived copy)
    {
        Derived* derived = static_cast<Derived*>(this);
        derived->swap(copy);
        return *derived;
    }
};

Я намерен его смешивать через CRTP:

struct Foo : copy_and_swap<Foo>
{
    Foo()
    {
        std::cout << "default\n";
    }

    Foo(const Foo& other)
    {
        std::cout << "copy\n";
    }

    void swap(Foo& other)
    {
        std::cout << "swap\n";
    }
};

Однако простой тест показывает, что он не работает:

Foo x;
Foo y;
x = y;

Это только дважды печатает "по умолчанию", не печатается ни "копия", ни "своп". Что мне здесь не хватает?

Ответ 1

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

Независимо от того, что вы делаете, вы находитесь в любом из двух случаев:

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

Таким образом, следующий вопрос: стоит ли автоматизировать такое письмо?

Copy-And-Swap используется только для очень специфических классов. Я не думаю, что это того стоит.

Ответ 2

Это:

 Derived& operator=(Derived copy)

не объявляет оператор присваивания копии базовому классу (он имеет неправильную подпись). Таким образом, созданный по умолчанию оператор присваивания в Foo не будет использовать этот оператор.

Помните 12.8:

Пользовательский оператор назначения копирования X:: operator = является нестационарным не-шаблонная функция класса X с точно одним параметром тип X, X &, const X &, летучий X & или const volatile X &.) [Примечание: перегруженный оператор присваивания должен быть объявлен только одним параметр; см. 13.5.3. ] [Примечание: более одной формы назначения копии оператор может быть объявлен для класса. ] [Примечание: если класс X имеет только оператор присваивания копии с параметром типа X &, выражение тип const X не может быть назначен объекту типа X.

EDIT не делайте этого (вы можете понять почему?):

Вы можете сделать:

template<typename Derived>
struct copy_and_swap
{
    void operator=(const copy_and_swap& copy)
    {
        Derived copy(static_cast<const Derived&>(copy));
        copy.swap(static_cast<Derived&>(*this));
    }
};

но вы теряете потенциальную оптимизацию при копировании.

В самом деле, это будет дважды назначать члены производных классов: один раз через copy_and_swap<Derived> оператор присваивания и один раз через сгенерированный оператор присваивания производного класса. Чтобы исправить ситуацию, вам придется делать (и не забывать делать):

struct Foo : copy_and_swap<Foo>
{

    Foo& operator=(const Foo& x)
    {
        static_cast<copy_and_swap<Foo>&>(*this) = x;
        return *this;
    }

private:
    // Some stateful members here
}

Мораль истории: не пишите класс CRTP для идиомы копирования и подкачки.

Ответ 3

Вы не можете наследовать операции присваивания как особый случай, если память правильно обслуживается. Я считаю, что они могут быть явно using 'd, если вам нужно.

Кроме того, будьте осторожны при использовании копирования и замены. Он создает неидеальные результаты, когда у оригинала есть ресурсы, которые можно было бы повторно использовать для создания копии, например, контейнеров. Безопасность гарантирована, но оптимальная производительность - нет.

Ответ 4

Компилятор автоматически создает оператор присваивания копии для Foo, поскольку его нет. Если вы добавите

    using copy_and_swap<Foo>::operator=;

to Foo вы увидите ошибку, сообщающую вам о двусмысленности в g++.

Ответ 5

Возможно, вы могли бы переписать его так, чтобы он выглядел так:

template<class Derived>
struct CopySwap
{
  Dervied &operator=(Derived const &other)
  {
    return AssignImpl(other);
  }

  Derived &operator=(Dervied &&other)
  {
    return AssignImpl(std::move(other));
  }

private:
  Derived &AssignImpl(Derived other)
  {
    auto self(static_cast<Derived*>(this));
    self->swap(other);
    return *self;
  }
};

Скорее всего, все получится в строгом соответствии и, скорее всего, не будет медленнее исходного кода.

Ответ 6

Это не отвечает на вопрос (@Alexandre C. уже сделал), но если вы отмените наследование, вы можете заставить его работать:

template<typename Base>
struct copy_and_swap : Base
{
    copy_and_swap& operator=(copy_and_swap copy)
    {
        swap(copy);
        return *this;
    }
};

struct Foo_
{
    Foo_()
    {
        std::cout << "default\n";
    }

    Foo_(const Foo_& other)
    {
        std::cout << "copy\n";
    }

    void swap(Foo_& other)
    {
        std::cout << "swap\n";
    }
};

typedef copy_and_swap<Foo_> Foo;

int main()
{
    Foo x;
    Foo y;
    x = y;
}