Подтвердить что ты не робот

Как реализовать правило пяти?

UPDATE внизу

q1: Как бы вы реализовали правило из пяти вариантов для класса, который управляет довольно тяжелыми ресурсами, но вы хотите, чтобы его передавали по значению, потому что это значительно упрощает и украшает его использование? Или нужны еще не все пять элементов правила?

На практике я начинаю что-то с 3D-изображением, где изображение обычно составляет 128 * 128 * 128 удваивается. Быть способным писать такие вещи, чтобы сделать математику намного проще:

Data a = MakeData();
Data c = 5 * a + ( 1 + MakeMoreData() ) / 3;

q2: Используя комбинацию семантики copy elision/RVO/move, компилятор должен уметь это с минимальным копированием, не?

Я попытался выяснить, как это сделать, поэтому я начал с основ; предположим, что объект реализует традиционный способ реализации копирования и назначения:

class AnObject
{
public:
  AnObject( size_t n = 0 ) :
    n( n ),
    a( new int[ n ] )
  {}
  AnObject( const AnObject& rh ) :
    n( rh.n ),
    a( new int[ rh.n ] )
  {
    std::copy( rh.a, rh.a + n, a );
  }
  AnObject& operator = ( AnObject rh )
  {
    swap( *this, rh );
    return *this;
  }
  friend void swap( AnObject& first, AnObject& second )
  {
    std::swap( first.n, second.n );
    std::swap( first.a, second.a );
  }
  ~AnObject()
  {
    delete [] a;
  }
private:
  size_t n;
  int* a;
};

Теперь введите rvalues ​​и переместите семантику. Насколько я могу судить, это будет рабочая реализация:

AnObject( AnObject&& rh ) :
  n( rh.n ),
  a( rh.a )
{
  rh.n = 0;
  rh.a = nullptr;
}

AnObject& operator = ( AnObject&& rh )
{
  n = rh.n;
  a = rh.a;
  rh.n = 0;
  rh.a = nullptr;
  return *this;
}

Однако компилятор (VС++ 2010 SP1) не слишком доволен этим, и компиляторы обычно правильны:

AnObject make()
{
  return AnObject();
}

int main()
{
  AnObject a;
  a = make(); //error C2593: 'operator =' is ambiguous
}

q3: Как это решить? Возвращаясь к AnObject & operator = (const AnObject & rh), конечно, исправляет его, но разве мы не теряем довольно важную возможность оптимизации?

Кроме того, он ясно, что код для конструктора и назначения перемещения полон дублирования. Поэтому на данный момент мы забываем о двусмысленности и пытаемся решить эту проблему с помощью копирования и свопинга, но теперь для rvalues. Как объяснено here, нам даже не нужен пользовательский своп, но вместо этого std:: swap выполняет всю работу, что звучит очень многообещающе. Поэтому я написал следующее, надеясь, что std:: swap скопирует временную конструкцию с помощью конструктора move, а затем поменяет его на * this:

AnObject& operator = ( AnObject&& rh )
{
  std::swap( *this, rh );
  return *this;
}

Но это не сработает и вместо этого приводит к переполнению стека из-за бесконечной рекурсии, поскольку std:: swap снова вызывает наш оператор = (AnObject && rh). q4: Может ли кто-нибудь представить пример того, что имеется в примере в этом примере?

Мы можем решить это, предоставив вторую функцию подкачки:

AnObject( AnObject&& rh )
{
  swap( *this, std::move( rh ) );
}

AnObject& operator = ( AnObject&& rh )
{
  swap( *this, std::move( rh ) );
  return *this;
}

friend void swap( AnObject& first, AnObject&& second )
{
  first.n = second.n;
  first.a = second.a;
  second.n = 0;
  second.a = nullptr;
}

Теперь там почти в два раза больше кода суммы, однако часть его движения платит за счёт довольно дешевого перемещения; но, с другой стороны, обычное назначение больше не может извлечь выгоду из копирования. На данный момент я действительно запутался, и, не видя больше, что правильно и что не так, я надеюсь получить здесь немного информации.

UPDATE Итак, кажется, что есть два лагеря:

  • одно высказывание, чтобы пропустить оператор присваивания перемещения и продолжать делать то, что нам учил С++ 03, т.е. написать один оператор присваивания, который передает аргумент по значению.
  • другой, говорящий, чтобы реализовать оператор назначения перемещения (в конце концов, это С++ 11 сейчас) и оператор присваивания копии принимает свой аргумент по ссылке.

(хорошо, и там 3-й лагерь говорит мне использовать вектор, но этот вид вне сферы действия для этого гипотетического класса. Хорошо в реальной жизни я бы использовал вектор, и были бы и другие члены, но так как move constructor/assign не генерируются автоматически (пока?) вопрос все равно будет сохраняться)

К сожалению, я не могу протестировать обе реализации в реальном мире, так как этот проект только начался, и способ фактического потока данных пока неизвестен. Поэтому я просто реализовал оба из них, добавил счетчики для выделения и т.д. И выполнил пару итераций ок. этот код, где T является одной из реализаций:

template< class T >
T make() { return T( narraySize ); }

template< class T >
void assign( T& r ) { r = make< T >(); }

template< class T >
void Test()
{
  T a;
  T b;
  for( size_t i = 0 ; i < numIter ; ++i )
  {
    assign( a );
    assign( b );
    T d( a );
    T e( b );
    T f( make< T >() );
    T g( make< T >() + make< T >() );
  }
}

Либо этот код недостаточно хорош, чтобы проверить, что мне нужно, либо компилятор слишком умный: не имеет значения, что я использую для arraySize и numIter, результаты для обоих лагерей в значительной степени идентичны: одинаковое число распределений, очень незначительные изменения в сроках, но не воспроизводимые существенные различия.

Поэтому, если кто-то не может указать на лучший способ проверить это (учитывая, что фактическое использование scnearios пока не известно), я должен сделать вывод, что это не имеет значения и, следовательно, остается на вкус разработчика, В этом случае я бы выбрал № 2.

4b9b3361

Ответ 1

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

  AnObject& operator = ( const AnObject& rh )
  {
    if (this != &rh)
    {
      if (n != rh.n)
      {
         delete [] a;
         n = 0;
         a = new int [ rh.n ];
         n = rh.n;
      }
      std::copy(rh.a, rh.a+n, a);
    }
    return *this;
  }

Если вы действительно не думаете, что присвоите AnObject того же размера, это намного лучше. Никогда не отбрасывайте ресурсы, если вы можете их переработать.

Некоторые могут пожаловаться на то, что оператор копирования AnObject теперь имеет только базовую безопасность исключений, а не безопасность сильных исключений. Однако рассмотрите это:

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

template <class T>
T&
strong_assign(T& x, T y)
{
    swap(x, y);
    return x;
}

Конструктор перемещения в порядке, но ваш оператор назначения перемещения имеет утечку памяти. Это должно быть:

  AnObject& operator = ( AnObject&& rh )
  {
    delete [] a;
    n = rh.n;
    a = rh.a;
    rh.n = 0;
    rh.a = nullptr;
    return *this;
  }

...

Data a = MakeData();
Data c = 5 * a + ( 1 + MakeMoreData() ) / 3;

q2: Используя комбинацию семантики copy elision/RVO/move компилятор должен уметь это с минимальным копированием, не?

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

Data operator+(Data&& x, const Data& y)
{
   // recycle resources in x!
   x += y;
   return std::move(x);
}

В конечном итоге ресурсы должны создаваться ровно один раз для каждого Data, о котором вы заботитесь. Там не должно быть ненужного new/delete только для перемещения вещей.

Ответ 2

Если ваш объект ресурсоемкий, вам может вообще не понадобиться копировать и просто предоставить конструктор перемещения и переместить оператор присваивания. Однако, если вы действительно хотите копировать, легко выполнить все операции.

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

AnObject( AnObject&& rh ) :
  n( std::move(rh.n) ),
  a( std::move(rh.a) )
{
  rh.n = 0;
  rh.a = nullptr;
}

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

Если вы предоставляете конструктор move, тогда вам не нужен оператор присваивания переадресации, когда вы определяете назначение копирования, как у вас, потому что вы принимаете параметр значение, rvalue будет перемещен в параметр, а не скопирован.

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

AnObject& operator=(AnObject other)
{
    std::swap(n,other.n);
    std::swap(a,other.a);
    return *this;
}

Таким образом, ваш последний класс:

class AnObject
{
public:
  AnObject( size_t n = 0 ) :
    n( n ),
    a( new int[ n ] )
  {}
  AnObject( const AnObject& rh ) :
    n( rh.n ),
    a( new int[ rh.n ] )
  {
    std::copy( rh.a, rh.a + n, a );
  }
  AnObject( AnObject&& rh ) :
    n( std::move(rh.n) ),
    a( std::move(rh.a) )
  {
    rh.n = 0;
    rh.a = nullptr;
  }
  AnObject& operator = ( AnObject rh )
  {
    std::swap(n,rh.n);
    std::swap(a,rh.a);
    return *this;
  }
  ~AnObject()
  {
    delete [] a;
  }
private:
  size_t n;
  int* a;
};

Ответ 3

Позвольте мне помочь вам:

#include <vector>

class AnObject
{
public:
  AnObject( size_t n = 0 ) : data(n) {}

private:
  std::vector<int> data;
};

Из С++ 0x FDIS, [class.copy] примечание 9:

Если определение класса X явно не объявляет конструктор перемещения, оно будет объявлено как неявное как дефолтное, если и только если

  • X не имеет объявленного пользователем конструктора копирования,

  • X не имеет объявленного пользователем оператора назначения копирования,

  • X не имеет объявленного пользователем оператора назначения перемещения,

  • X не имеет объявленного пользователем деструктора и

  • конструктор перемещения не будет неявно определен как удаленный.

[Примечание. Если конструктор перемещения неявно объявлен или явно указан, выражения, которые в противном случае вызывают конструктор перемещения, могут вместо этого вызвать конструктор копирования. -end note]

Лично я увереннее в std::vector правильном управлении своими ресурсами и оптимизации копий/ходов, которые я могу написать в любом коде.

Ответ 4

Поскольку я не видел, чтобы кто-либо еще явно указывал на это...

Оператор присваивания копий, принимающий его аргумент по значению, является важной возможностью оптимизации, если (и только, если) он передал значение r из-за копирования. Но в классе с оператором присваивания, который явно только принимает значения r (например, один с оператором присваивания перемещения), это бессмысленный сценарий. Таким образом, по модулю утечек памяти, которые уже были указаны в других ответах, я бы сказал, что ваш класс уже идеален, если вы просто меняете оператор присваивания копий, чтобы принять его аргумент по ссылке const.

Ответ 5

q3 оригинального плаката

Я думаю, что вы (и некоторые другие респонденты) неправильно поняли, что означала ошибка компилятора, и пришли к неправильным выводам из-за этого. Компилятор считает, что вызов назначения (перемещение) неоднозначен, и это правильно! У вас есть несколько методов, которые одинаково квалифицированы.

В исходной версии класса AnObject ваш конструктор копий берет старый объект с помощью ссылки const (lvalue), а оператор присваивания принимает свой аргумент по (неквалифицированному) значению. Аргумент значения инициализируется соответствующим конструктором переноса из того, что было справа от оператора. Поскольку у вас есть только один конструктор переноса, этот конструктор копирования всегда используется, независимо от того, было ли исходное правое выражение равным lvalue или rvalue. Это приводит к тому, что оператор присваивания действует как специальная функция-функция присваивания копиям.

Ситуация меняется после добавления конструктора перемещения. Всякий раз, когда вызывается оператор присваивания, для конструктора переноса есть два варианта. Конструктор копирования по-прежнему будет использоваться для выражений lvalue, но конструктор перемещения будет использоваться всякий раз, когда вместо него указывается выражение rvalue! Это приводит к тому, что оператор присваивания одновременно действует как специальная функция-назначение переадресации.

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

В двух лагерях, перечисленных в вашем обновлении, я предполагаю, что я технически в первом лагере, но по совершенно другим причинам. (Не пропустите (традиционный) оператор перемещения-назначения, потому что он "сломан" для вашего класса, но потому что он лишний.)

Кстати, я новичок в чтении о С++ 11 и StackOverflow. Я придумал этот ответ, просмотрев еще один S.O. вопрос перед тем посмотрев этот. ( Обновить: на самом деле, у меня все еще была страница. Ссылка идет на конкретный ответ FredOverflow, который показывает технику.)

О реакции 2011-май-12 Говарда Хиннанта

(Я слишком много новичок, чтобы напрямую комментировать ответы).

Вам не нужно явно проверять самоопределение, если более поздний тест уже будет отбирать его. В этом случае n != rh.n уже позаботится об этом. Однако вызов std::copy находится за пределами этого (текущего) внутреннего if, поэтому мы получим n самоначисления на уровне компонента. Вам решать, будут ли эти назначения слишком анти-оптимальными, даже если самозапуск должен быть редок.

Ответ 6

С помощью делегирования конструктора вам нужно только реализовать каждую концепцию один раз,

  • default init
  • удалить ресурс
  • своп
  • Копия

остальные просто используют их.

Также не забудьте сделать move-assign (и swap) noexcept, если помогает производительность, если вы, например, ставите свой класс в a vector

#include <utility>

// header

class T
{
public:
    T();
    T(const T&);
    T(T&&);
    T& operator=(const T&);
    T& operator=(T&&) noexcept;
    ~T();

    void swap(T&) noexcept;

private:
    void copy_from(const T&);
};

// implementation

T::T()
{
    // here (and only here) you implement default init
}

T::~T()
{
    // here (and only here) you implement resource delete
}

void T::swap(T&) noexcept
{
    using std::swap; // enable ADL
    // here (and only here) you implement swap, typically memberwise swap
}

void T::copy_from(const T& t)
{
    if( this == &t ) return; // don't forget to protect against self assign
    // here (and only here) you implement copy
}

// the rest is generic:

T::T(const T& t)
    : T()
{
    copy_from(t);
}

T::T(T&& t)
    : T()
{
    swap(t);
}

auto T::operator=(const T& t) -> T&
{
    copy_from(t);
    return *this;
}

auto T::operator=(T&& t) noexcept -> T&
{
    swap(t);
    return *this;
}