Реализация подкачки в идиоме копирования и подкачки

После Что такое идиома копирования и подкачки и Как предоставить функцию подкачки для моего класса, я попытался реализовать подкачку функция, как в последнем, приняла вариант ответа номер 2 (имея бесплатную функцию, которая вызывает функцию-член) вместо прямой дружественной бесплатной функции в прежней ссылке.

Однако следующее не компилируется

#include <iostream>

// Uncommenting the following two lines won't change the state of affairs
// class Bar;
// void swap(Bar &, Bar &);
class Bar {
public:
  Bar(unsigned int bottles=0) : bottles(bottles) { enforce(); } // (1)
  Bar(Bar const & b) : bottles(b.bottles) { enforce(); } // (1)

  Bar & operator=(Bar const & b) {
    // bottles = b.bottles;
    // enforce();
    // Copy and swap idiom (maybe overkill in this example)
    Bar tmp(b); // but apart from resource management it allows (1)
                // to enforce a constraint on the internal state
    swap(*this, tmp); // Can't see the swap non-member function (2)
    return *this;
  }

  void swap(Bar & that) {
    using std::swap;
    swap(bottles, that.bottles);
  }

  friend std::ostream & operator<<(std::ostream & out, Bar const & b) {
    out << b.bottles << " bottles";
    return out;
  }

private:
  unsigned int bottles;
  void enforce() { bottles /=2; bottles *= 2; } // (1) -- Ensure the number of bottles is even
};

void swap(Bar & man, Bar & woman) { // (2)
  man.swap(woman);
}

int main () {
  Bar man (5);
  Bar woman;

  std::cout << "Before -> m: " << man << " / w: " << woman << std::endl;
  swap(man, woman);
  std::cout << "After  -> m: " << man << " / w: " << woman << std::endl;

  return 0;
}

Я знаю, что идиома копирования и подкачки здесь излишня, но она также позволяет наложить некоторые ограничения на внутреннее состояние с помощью конструктора копирования (1) (более конкретным примером может быть сохранение дроби в сокращенной форме). К сожалению, это не компилируется, потому что единственным кандидатом на (2), который видит компилятор, является функция-член Bar::swap. Я застрял с подходом функции друга, не являющегося членом?

РЕДАКТИРОВАТЬ: перейдите к моему ответу ниже, чтобы увидеть, что у меня получилось, благодаря всем ответам и комментариям по этому вопросу.


person green diod    schedule 15.03.2016    source источник
comment
Ваш код никоим образом не соответствует успешному решению в ответе GMan.   -  person Cody Gray    schedule 15.03.2016
comment
если вы правильно реализуете оператор присваивания перемещения и конструктор перемещения, нет необходимости реализовывать своп.   -  person Richard Hodges    schedule 15.03.2016
comment
Внутри класса, почему бы просто не вызвать this->swap(other)?   -  person Yakk - Adam Nevraumont    schedule 15.03.2016
comment
Если бы когда-нибудь был хороший пример слайда 50 из этой коллекции слайдов (slideshare.net/ripplelabs /howard-hinnant-accu2014), это пример. Установите конструктор копирования и оператор присваивания по умолчанию, и вы получите оптимальный и правильный код. Вы можете сделать это, просто не упоминая их. Компилятор неявно объявит и определит оптимальный код.   -  person Howard Hinnant    schedule 16.03.2016
comment
@HowardHinnant Мне понравились слайды, спасибо, что поделились ими, жаль, что нет видео.   -  person green diod    schedule 16.03.2016


Ответы (5)


Я так понимаю, мы пост С++ 11?

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

http://en.cppreference.com/w/cpp/algorithm/swap

#include <iostream>

class Bar {
public:
    Bar(unsigned int bottles=0) : bottles(bottles) { enforce(); } // (1)
    Bar(Bar const & b) : bottles(b.bottles) {
        // b has already been enforced. is enforce necessary here?
        enforce();
    } // (1)
    Bar(Bar&& b) noexcept
    : bottles(std::move(b.bottles))
    {
        // no need to enforce() because b will have already been enforced;
    }

    Bar& operator=(Bar&& b) noexcept
    {
        auto tmp = std::move(b);
        swap(tmp);
        return *this;
    }

    Bar & operator=(Bar const & b)
    {
        Bar tmp(b); // but apart from resource management it allows (1)
        swap(tmp);
        return *this;
    }

    void swap(Bar & that) noexcept {
        using std::swap;
        swap(bottles, that.bottles);
    }

    friend std::ostream & operator<<(std::ostream & out, Bar const & b) {
        out << b.bottles << " bottles";
        return out;
    }

private:
    unsigned int bottles;
    void enforce() {  } // (1)
};

/* not needed anymore
void swap(Bar & man, Bar & woman) { // (2)
    man.swap(woman);
}
*/
int main () {
    Bar man (5);
    Bar woman;

    std::cout << "Before -> m: " << man << " / w: " << woman << std::endl;
    using std::swap;
    swap(man, woman);
    std::cout << "After  -> m: " << man << " / w: " << woman << std::endl;

    return 0;
}

ожидаемый результат:

Before -> m: 5 bottles / w: 0 bottles
After  -> m: 0 bottles / w: 5 bottles

РЕДАКТИРОВАТЬ:

В интересах всех, кто беспокоится о производительности (например, @JosephThompson), позвольте мне развеять ваши опасения. После переноса вызова std::swap в виртуальную функцию (чтобы заставить clang вообще создавать какой-либо код) и компиляции с помощью apple clang с -O2 это:

void doit(Bar& l, Bar& r) override {
    std::swap(l, r);
}

стало так:

__ZN8swapper24doitER3BarS1_:            ## @_ZN8swapper24doitER3BarS1_
    .cfi_startproc
## BB#0:
    pushq   %rbp
Ltmp85:
    .cfi_def_cfa_offset 16
Ltmp86:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp87:
    .cfi_def_cfa_register %rbp
    movl    (%rsi), %eax
    movl    (%rdx), %ecx
    movl    %ecx, (%rsi)
    movl    %eax, (%rdx)
    popq    %rbp
    retq
    .cfi_endproc 

Видеть? оптимальный. Стандартная библиотека С++ рулит!

person Richard Hodges    schedule 15.03.2016
comment
Да для С++ 11 (я использую gcc -std=С++ 14). На самом деле, я был бы вполне доволен оператором = по умолчанию, но для дополнительного принудительного применения ограничений, которое я хочу для внутреннего состояния класса. Конечно, я согласен с вашими комментариями о необходимости (или нет) принудительного выполнения() в некоторых специальных функциях. Особенно в контексте этого примера, где бутылки довольно быстро взлетели бы до небес! - person green diod; 15.03.2016
comment
Однако, если Bar унаследован от Foo, и если бы у меня было что-то вроде этого Bar(Foo const &) (скажите мне, плохой ли это дизайн), возможно, я хотел бы убедиться, что force() все еще вызывается, например. Bar(Foo const & f) : Foo(f) { enforce(); } - person green diod; 15.03.2016
comment
конечно, это нормально. Просто если вы строите из Бара, то для того, чтобы он существовал, этот Бар должен быть действительным, так как он прошел свою собственную enforce проверку. поэтому вызов enforce в конструкторе копирования не является необходимым (при условии принудительного выполнения бросков при сбое, как и должно быть) - person Richard Hodges; 15.03.2016
comment
Думая об этом, если бы я только что построил из того же класса, я бы просто исключил все эти функции, кроме конструктора прямой инициализации, который был бы единственным, обеспечивающим соблюдение ограничения на внутреннее состояние. Но спасибо за развертывание всех 4 с половиной функций, я думаю, что они очень полезны, если я также конструирую из унаследованных классов. - person green diod; 15.03.2016
comment
Если перемещение объекта точно такое же, как его копирование, то std::swap будет оптимально эффективным. Если нет, то я думаю, что вы обычно можете добиться большего успеха с пользовательским swap. В вашем примере я думаю, что std::swap выполняет 9 целочисленных копий. Вы могли бы уменьшить это число до 5, если бы в Bar перемещенном из Bar было ноль бутылок. Если переместить из Bar оставить без изменений (т. е. переместить == копировать), вы получите оптимальные 3 целочисленные копии. - person Joseph Thomson; 15.03.2016
comment
@RichardHodges Я изменил enforce, чтобы все барные тендеры имели бутылки, кратные 2. Теперь применение более одного раза больше не меняет бутылки. Конечно, ваше замечание остается в силе. - person green diod; 15.03.2016
comment
@JosephThomson Я не совсем понимаю твой ход == копия. Не могли бы вы уточнить это? - person green diod; 15.03.2016
comment
@JosephThomson не с С++ 11. Если стандартная библиотека обнаруживает, что у вас есть конструктор перемещения и оператор присваивания перемещения, то std::move реализует себя с их точки зрения. - person Richard Hodges; 15.03.2016
comment
@RichardHodges Да, поэтому std::swap(a, b) переместит конструкцию a во временную, переместит назначение b в a, а затем переместит временное назначение во b. Это построение одного хода и назначение двух ходов. Ваш конструктор перемещения создает одну копию bottles, а оператор присваивания перемещения создает четыре копии bottles. Всего получается девять целочисленных копий, что менее эффективно, чем просто вызов a.swap(b), который выполнит три целочисленных копии. - person Joseph Thomson; 15.03.2016
comment
@greendiod Если бы перемещение Bar было таким же, как копирование Bar, то std::swap по существу выполнило бы три копии Bar, что было бы оптимально эффективно (три целочисленные копии) в этом случае. - person Joseph Thomson; 15.03.2016
comment
@JosephThomson в неоптимизированном коде, да. Я согласен. - person Richard Hodges; 15.03.2016
comment
@RichardHodges Даже в оптимизированном коде, если перемещение не то же самое, что копирование, вы, вероятно, можете реализовать собственный swap, который более оптимален, чем реализация std::swap по умолчанию. То есть, если компилятор не сможет оптимизировать ненужные операции (вполне вероятно в этом простом случае). - person Joseph Thomson; 15.03.2016
comment
@RichardHodges Конечно, для этого простого примера компилятор может полностью оптимизировать операцию std::swap, но в общем случае это невозможно. Например, попробуйте превратить bottles в std::unique_ptr. Bar::swap по-прежнему всего четыре инструкции перемещения, тогда как std::swap даже вызывает operator delete. - person Joseph Thomson; 15.03.2016
comment
@JosephThomson Вы считаете копии или ходы? Как вы их считаете? Может быть, у вас есть блог об этом? Любая ссылка будет принята с благодарностью. - person green diod; 16.03.2016
comment
@greendiod Я говорю об инструкциях по сборке movl, а не об операциях перемещения C++. Скопируйте этот код в gcc.godbolt.org и скомпилируйте с помощью -std=c++14 -O2 (Clang или GCC), затем проверьте сборку на наличие std_swap и custom_swap. Если подумать, operator delete, очевидно, не вызывается ни в какой момент во время обмена, но я по-прежнему считаю, что компилятору не удается сгенерировать оптимальный код. Конечно, я просто рассуждаю с технической точки зрения ;). Пожалуйста, оцените, прежде чем тратить время на преждевременную оптимизацию. - person Joseph Thomson; 16.03.2016
comment
@JosephThomson, возможно, мне следовало написать близко к оптимальному - person Richard Hodges; 16.03.2016
comment
@RichardHodges Ха-ха, да. Я согласен с тем, что пост-С++ 11 std::swap достаточно хорош в подавляющем большинстве случаев. Если вы не пишете высокооптимизированный библиотечный код или не имеете контрольных показателей для резервного копирования, написание пользовательского swap в дополнение к операциям перемещения, несомненно, является преждевременной оптимизацией. Я просто пытался быть технически правильным: лучший вид правильного :) - person Joseph Thomson; 16.03.2016
comment
@JosephThomson Я не понял, что вы переходите на более низкий уровень, даже если обновление Ричарда Ходжеса должно было указать мне в этом направлении: P В любом случае, это действительно интересно, так что еще раз спасибо! - person green diod; 16.03.2016

Примечание. Это способ использования копирования и подкачки, существовавший до C++11. Для решения C++11 см. этот ответ

Чтобы заставить это работать, вам нужно исправить пару вещей. Сначала вам нужно предварительно объявить функцию swap free, чтобы operator= знал об этом. Для этого вам также необходимо предварительно объявить Bar, чтобы swap был тип с именем bar

class Bar;

void swap(Bar & man, Bar & woman);

// rest of code

Затем нам нужно указать компилятору, где искать swap. Мы делаем это с помощью оператора разрешения области видимости. Это укажет компилятору искать в области видимости класса функцию swap.

Bar & operator=(Bar const & b) {
  // bottles = b.bottles;
  // enforce();
  // Copy and swap idiom (maybe overkill in this example)
  Bar tmp(b); // but apart from resource management it allows (1)
            // to enforce a constraint on the internal state
  ::swap(*this, tmp); // Can't see the swap non-member function (2)
//^^ scope operator 
  return *this;
}

Собираем все вместе и получаем этот живой пример.

Действительно, хотя копия operator = должна выглядеть так

Bar & operator=(Bar b) // makes copy
{
    ::swap(*this, b) // swap the copy
    return *this; // return the new value
}
person NathanOliver    schedule 15.03.2016
comment
... хотя этот ответ закрывает дверь конюшни после того, как лошадь убежала, не так ли? почему бы просто не использовать обмен функциями-членами внутри функций-членов? - person Richard Hodges; 15.03.2016
comment
@RichardHodges ОП, похоже, не хочет иметь функцию-член или функцию друга. Похоже, он использует вариант 2 из этого ответа. Я не знаю, какой способ на самом деле лучше, или можно ли сказать, что один способ лучше другого, поэтому я представил это, чтобы заставить его работать. - person NathanOliver; 15.03.2016
comment
@NathanOliver Извините, я имел в виду бутылки * = 2, здесь это немного надумано, но принудительное () будет сокращением () в классе Fraction, поддерживаемом в сокращенной форме. - person green diod; 15.03.2016
comment
@greendiod Хорошо. Я исправил это в живом примере, поэтому теперь он компилируется. - person NathanOliver; 15.03.2016
comment
@NathanOliver Хорошо, у меня было предварительное объявление, но не хватало оператора области видимости. Мне также нравится ваш перегруженный оператор =, где b передается по значению, чтобы гарантировать вызов конструктора копирования, обеспечивающего соблюдение ограничений! - person green diod; 15.03.2016
comment
@NathanOliver понял, но реализация обмена ADL в наши дни немного старомодна. Поскольку С++ 11, нет необходимости, если класс можно назначать и перемещать. Ответ ниже. - person Richard Hodges; 15.03.2016
comment
@RichardHodges Я думаю, что мое редактирование поможет прояснить это. - person NathanOliver; 15.03.2016

Вы знаете, что Bar имеет функцию-член swap, поэтому просто вызовите ее напрямую.

Bar& operator=(Bar const& b) {
    Bar tmp(b);
    tmp.swap(*this);
    return *this;
}

Нечлен swap существует только для того, чтобы клиенты Bar могли воспользоваться преимуществами его оптимизированной реализации swap, не зная, существует ли она, используя идиому using std::swap для включения поиск в зависимости от аргумента:

using std::swap;
swap(a, b);
person Joseph Thomson    schedule 15.03.2016
comment
Вы имеете в виду, что внутри Bar я должен просто использовать функцию-член swap, а для клиентов Bar я должен предоставить функцию swap, не являющуюся членом? - person green diod; 15.03.2016
comment
да. Вызов не-члена swap из вашего класса кажется немного бессмысленным, так как он все равно вызывает член swap. Подкачка нечлена предназначена только для поддержки идиомы using std::swap для включения ADL. Это особенно полезно в универсальном коде, где вы заменяете объекты неизвестного типа. - person Joseph Thomson; 15.03.2016

Вам также необходимо включить std::swap в этой функции.

using std::swap;
swap(*this, tmp); // Can't see the swap non-member function (2)

Цитируя ответ, на который вы ссылались:

Если теперь используется своп, как показано в 1), ваша функция будет найдена.

Способ его использования:

{
  using std::swap; // enable 'std::swap' to be found
                   // if no other 'swap' is found through ADL
  // some code ...
  swap(lhs, rhs); // unqualified call, uses ADL and finds a fitting 'swap'
                  // or falls back on 'std::swap'
  // more code ...
}

Прямая трансляция на Coliru

person hlscalon    schedule 15.03.2016

Для приведенного выше контекста, где нужно только применить какое-то внутреннее ограничение, лучше использовать значение по умолчанию и просто применять ограничение только один раз в конструкторе прямой инициализации. Тем не менее, если вам нужно реализовать эти функции, посмотрите ответ @RichardHodges! См. также комментарий @HowardHinnant (особенно часть слайдов, когда компилятор делает магию неявно объявляет специальные члены...).

Вот что у меня получилось (без явного копирования и подкачки):

#include <iostream>

class Bar {
public:
  Bar(unsigned int bottles=0) : bottles(bottles) { enforce(); } // The only point of enforcement

  friend std::ostream & operator<<(std::ostream & out, Bar const & b) {
    out << b.bottles << " bottles";
    return out;
  }

private:
  unsigned int bottles;
  void enforce() { bottles /= 2; bottles *=2; }
};

int main () {
  Bar man (5);
  Bar woman;

  std::cout << "Before -> m: " << man << " / w: " << woman << std::endl;
  using std::swap; // Argument dependent lookup
  swap(man, woman);
  std::cout << "After  -> m: " << man << " / w: " << woman << std::endl;

  return 0;
}

Теперь, что произойдет, если Bar наследуется от Foo (которому не нужен enforce). Это первоначальный вариант использования, который заставил меня подумать, что мне нужно развернуть свои собственные специальные функции и извлечь выгоду из копирующей части копии и заменить идиому на enforce ограничение. Получается, что даже в этом случае мне не нужно:

#include <iostream>

class Foo {
public:
  Foo(unsigned int bottles=11) : bottles(bottles) {} // This is odd on purpose

  virtual void display(std::ostream & out) const {
    out << bottles << " bottles";
  }

protected:
  unsigned int bottles;
};

std::ostream & operator<<(std::ostream & out, Foo const & f) {
  f.display(out);
  return out;
}

class Bar : public Foo {
public:
  Bar(unsigned int bottles=0) : Foo(bottles) { enforce(); }
  Bar(Foo const & f) : Foo(f) { enforce(); }

  void display(std::ostream & out) const override {
    out << bottles << " manageable bottles";
  }

private:
  void enforce() { bottles /= 2; bottles *=2; }
};

int main () {
  Bar man (5); // Again odd on purpose
  Bar woman;

  std::cout << "Before -> m: " << man << " / w: " << woman << std::endl;
  using std::swap; // Argument dependent lookup
  swap(man, woman);
  std::cout << "After  -> m: " << man << " / w: " << woman << std::endl;

  Foo fool(7); // Again odd
  Bar like(fool);
  std::cout << fool << " -> (copy) " << like << std::endl;
  Bar crazy;
  crazy = fool;
  std::cout << fool << " ->   (=)  " << crazy << std::endl;

  return 0;
}
person green diod    schedule 16.03.2016