Безопасное назначение и копирование и замена - PullRequest
9 голосов
/ 06 мая 2011

Я изучаю c ++, и недавно я узнал (здесь, в переполнении стека) об идиоме копирования и обмена, и у меня есть несколько вопросов по этому поводу. Итак, предположим, что у меня есть следующий класс, использующий идиому копирования и обмена, например:

class Foo {
private:
  int * foo;
  int size;

public:
  Foo(size_t size) : size(size) { foo = new int[size](); }
  ~Foo(){delete foo;}

  Foo(Foo const& other){
    size = other.size;
    foo = new int[size];
    copy(other.foo, other.foo + size, foo);
  }

  void swap(Foo& other) { 
    std::swap(foo,  other.foo);  
    std::swap(size, other.size); 
  }

  Foo& operator=(Foo g) { 
    g.swap(*this); 
    return *this; 
  }

  int& operator[] (const int idx) {return foo[idx];}
};

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

class Bar {
private:
  Foo bar;
public:
  Bar(Foo foo) : bar(foo) {};
  ~Bar(){};
  Bar(Bar const& other) : bar(other.bar) {}; 
  Bar& operator=(Bar other) {bar = other.bar;}
};

Теперь у меня есть ряд вопросов:

  1. Безопасны ли методы и конструкторы, реализованные выше для класса Bar? Воспользовавшись копированием и заменой для Foo, убедитесь, что при назначении или копировании Bar не может быть никакого вреда?

  2. Передача аргумента по ссылке в конструкторе копирования и в свопе является обязательной?

  3. Правильно ли говорить, что когда аргумент operator= передается по значению, конструктор копирования вызывается для этого аргумента, чтобы сгенерировать временную копию объекта, и что именно эта копия затем поменялся местами с *this? Если бы я прошел по ссылке в operator=, у меня была бы большая проблема, верно?

  4. Существуют ли ситуации, в которых эта идиома не обеспечивает полной безопасности при копировании и назначении Foo?

Ответы [ 3 ]

8 голосов
/ 06 мая 2011

Насколько это возможно, вы должны инициализировать членов вашего класса в списке инициализаторов. Это также позаботится об ошибке, о которой я говорил вам в комментарии. Имея это в виду, ваш код становится:

class Foo {
private:
  int size;
  int * foo;

public:
  Foo(size_t size) : size(size), foo(new int[size]) {}
  ~Foo(){delete[] foo;} // note operator delete[], not delete

  Foo(Foo const& other) : size(other.size), foo(new int[other.size]) {
    copy(other.foo, other.foo + size, foo);
  }

  Foo& swap(Foo& other) { 
    std::swap(foo,  other.foo);  
    std::swap(size, other.size); 
    return *this;
  }

  Foo& operator=(Foo g) { 
    return swap(g); 
  }

  int& operator[] (const int idx) {return foo[idx];}
};

и

class Bar {
private:
  Foo bar;
public:
  Bar(Foo foo) : bar(foo) {};
  ~Bar(){};
  Bar(Bar const& other) : bar(other.bar) { }
  Bar& swap(Bar &other) { bar.swap(other.bar); return *this; }
  Bar& operator=(Bar other) { return swap(other); }
}

, который использует одну и ту же идиому во всем

примечание

Как уже упоминалось в комментарии к вопросу, пользовательские конструкторы копирования Bar и т. Д. Не нужны, но мы будем предполагать, что Bar имеет и другие вещи: -)

второй вопрос

Переход по ссылке на swap необходим, поскольку оба экземпляра изменены.

Необходим переход по ссылке на конструктор копирования, потому что при передаче по значению вам потребуется вызвать конструктор копирования

третий вопрос

да

четвертый вопрос

нет, но это не всегда самый эффективный способ ведения дел

5 голосов
/ 06 мая 2011

1 - Безопасны ли методы и конструкторы, реализованные выше для класса Bar?Используя копирование и обмен для Foo, убедитесь, что при назначении или копировании Bar не может быть никакого вреда?

Относительно copy-ctor: это всегда безопасно (все или ничего),Он либо завершает (все), либо выдает исключение (ничего).

Если ваш класс состоит только из одного члена (то есть без базовых классов), оператор присваивания будет так же безопасен, какчто из класса члена.Если у вас более одного члена, оператор присваивания больше не будет «все или ничего».Оператор присваивания второго члена может выдать исключение, и в этом случае объект будет назначен «на полпути».Это означает, что вам нужно снова реализовать функцию копирования и замены в новом классе, чтобы получить назначение «все или ничего».

Однако это все равно будет «безопасным», в том смысле, что вы не потеряетеРесурсы.И, конечно, состояние каждого члена в отдельности будет согласованным - просто состояние нового класса не будет согласованным, поскольку один член был назначен, а другой - нет.

2 - ПередачаАргумент по ссылке в конструкторе копирования и в swap является обязательным?

Да, передача по ссылке обязательна.Конструктор копирования - это тот, который копирует объекты, поэтому он не может принимать свой аргумент по значению, поскольку это будет означать, что аргумент должен быть скопирован.Это привело бы к бесконечной рекурсии.(Copy-ctor будет вызываться для аргумента, что будет означать вызов copy-ctor для аргумента, что будет означать ...).Для свопа причина в другом: если вы передадите аргумент по значению, вы никогда не сможете использовать своп для реального обмена содержимым двух объектов - «целью» свопа будет копия объекта, изначально переданного,который будет немедленно уничтожен.

3 - Правильно ли говорить, что когда аргумент operator = передается по значению, конструктор копирования вызывается для этого аргумента, чтобы сгенерировать временную копию объектаи что именно эта копия затем поменялась местами с * this?Если бы я прошел по ссылке в операторе = у меня была бы большая проблема, верно?

Да, все верно.Тем не менее, также довольно распространено взять аргумент с помощью reference-to-const, создать локальную копию и затем заменить ее локальной копией.Однако способ by reference-to-const имеет некоторые недостатки (он отключает некоторые оптимизации).Если вы не реализуете функцию копирования и замены, вам, вероятно, следует перейти по ссылке на const.

4 - Существуют ли ситуации, в которых эта идиома не удаетсяобеспечить полную безопасность при копировании и назначении Foo?

Ничего из того, что я знаю.Конечно, с многопоточностью всегда можно что-то потерпеть (если не правильно синхронизированы), но это должно быть очевидно.

2 голосов
/ 06 мая 2011

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

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

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

Наблюдение: Когда size() == rhs.size(), освобождение или распределение не требуется.Все, что вам нужно сделать, это copy.Это намного , намного быстрее.

Foo& operator=(const Foo& g) {
    if (size != g.size)
        Foo(g).swap(*this); 
    else
       copy(other.foo, other.foo + size, foo);
    return *this; 
}

Т.е. проверьте случай, когда вы можете сначала утилизировать ресурсы.Затем скопируйте и поменяйте (или что-то еще), если вы не можете утилизировать свои ресурсы.

Примечание: Мои комментарии не противоречат ни хорошим ответам, данным другим, ни проблемам правильности.Мой комментарий только о значительном снижении производительности.

...