Как на самом деле реализовать правило пяти? - PullRequest
41 голосов
/ 12 мая 2011

ОБНОВЛЕНИЕ внизу

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

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

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

q2: Используя комбинацию семантики копирования 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;
}

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

AnObject make()
{
  return AnObject();
}

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

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

Кроме того, ясно, что код для конструктора перемещения и присваивания полондублирования.Итак, пока мы забываем о неоднозначности и пытаемся решить эту проблему, используя copy и swap, но теперь для rvalues.Как объяснено здесь , нам даже не понадобится пользовательский своп, а вместо этого будет работать std :: swap, что звучит очень многообещающе.Поэтому я написал следующее, надеясь, что std :: swap скопирует конструкцию временного объекта с использованием конструктора перемещения, а затем поменяет его на * 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;
}

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

ОБНОВЛЕНИЕ Так что, кажется, есть дваcamps:

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

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

К сожалению, я не могу протестировать обе реализации в реальном сценарии, так как этот проект только начался иКак на самом деле будут передаваться данные, пока неизвестно.Поэтому я просто реализовал их оба, добавил счетчики для распределения и т. Д. И выполнил пару итераций ок.этот код, где 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.

Ответы [ 6 ]

17 голосов
/ 12 мая 2011

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

  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: Используя комбинацию семантики копирования elision / RVO / move, компилятор должен:быть в состоянии сделать это с минимумом копирования, не так ли?

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

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

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

13 голосов
/ 12 мая 2011

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

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

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

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

Если вы предоставляете конструктор перемещения, то вы невам нужен оператор присваивания перемещения, когда вы определяете назначение копирования, как у вас - потому что вы принимаете параметр с помощью значение , значение r будет перемещено в параметр, а не скопировано.

Как вы обнаружили, вы не можете использовать 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;
};
4 голосов
/ 12 мая 2011

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

#include <vector>

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

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

Из C ++ 0x FDIS, [class.copy] note 9:

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

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

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

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

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

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

[Примечание: если конструктор перемещения не объявлен неявно или не предоставлен явно, выражения, которые в противном случае вызвали бы конструктор перемещения, могут вместо этого вызвать конструктор копирования. —Конечная записка]

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

1 голос
/ 10 февраля 2017

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

  • default init
  • удаление ресурса
  • swap
  • copy

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

Также не забудьте выполнить перемещение-задание (и обмен) noexcept, если вам, например, очень помогает производительность, например,поместите свой класс в 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;
}
1 голос
/ 31 октября 2011

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

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

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

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

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

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

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

Об ответе Говарда Хиннанта за 2011-май-12 года

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

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

1 голос
/ 12 мая 2011

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...