Неплохая ли форма вызова оператора присваивания по умолчанию из конструктора копирования?
Рассмотрим класс, копии которого необходимо сделать. Подавляющее большинство элементов данных в копии должны строго отражать оригинал, однако есть несколько элементов, состояние которых не должно сохраняться и нуждается в повторной инициализации.
Является ли плохая форма вызовом оператора присваивания по умолчанию из конструктора копирования?
Оператор присваивания по умолчанию будет хорошо вести себя с обычными старыми данными (int, double, char, short), а также определенными пользователем классами на их операторы присваивания. Указатели должны рассматриваться отдельно.
Один из недостатков заключается в том, что этот метод приводит к тому, что оператор присваивания искалечен, поскольку дополнительная повторная инициализация не выполняется. Также невозможно отключить использование оператора присваивания, тем самым открыв пользователю возможность создать сломанный класс, используя неполный оператор присваивания по умолчанию A obj1,obj2; obj2=obj1; /* Could result is an incorrectly initialized obj2 */
.
Было бы неплохо смягчить требование, чтобы в a(orig.a),b(orig.b)...
в дополнение к a(0),b(0) ...
нужно было записать. Необходимость записи всей инициализации дважды создает два места для ошибок, и если в класс должны быть добавлены новые переменные (например, double x,y,z
), код инициализации должен быть правильно добавлен как минимум в 2 места вместо 1.
Есть ли лучший способ?
Есть ли лучший способ в С++ 0x?
class A {
public:
A(): a(0),b(0),c(0),d(0)
A(const A & orig){
*this = orig; /* <----- is this "bad"? */
c = int();
}
public:
int a,b,c,d;
};
A X;
X.a = 123;
X.b = 456;
X.c = 789;
X.d = 987;
A Y(X);
printf("X: %d %d %d %d\n",X.a,X.b,X.c,X.d);
printf("Y: %d %d %d %d\n",Y.a,Y.b,Y.c,Y.d);
Вывод:
X: 123 456 789 987
Y: 123 456 0 987
Альтернативный конструктор копирования:
A(const A & orig):a(orig.a),b(orig.b),c(0),d(orig.d){} /* <-- is this "better"? */
Ответы
Ответ 1
Как подчеркивает Бронн, вам лучше реализовать назначение с точки зрения построения копии. Я предпочитаю ему альтернативную идиому:
T& T::operator=(T t) {
swap(*this, t);
return *this;
}
Это немного короче, и может воспользоваться некоторыми функциями эзотерического языка для повышения производительности. Как и любой хороший код С++, он также имеет некоторые тонкости для просмотра.
Во-первых, параметр t
намеренно передается по значению, так что конструктор копирования будет называться (большую часть времени) и мы можем изменить это на наше сердечное содержание, не влияя на исходное значение. Использование const T&
не скомпилировалось, а T&
вызвало бы удивительное поведение, изменив значение назначенного значения.
Этот метод также требует, чтобы swap
был специализированным для типа способом, который не использует оператор присваивания типа (как это делает std::swap
), или это приведет к бесконечной рекурсии. Будьте осторожны с любыми паразитными using std::swap
или using namespace std
, поскольку они будут тянуть std::swap
в область видимости и вызывать проблемы, если вы не специализируетесь на swap
для t
. Разрешение перегрузки и ADL обеспечит правильную версию swap, если вы ее определили.
Существует несколько способов определить swap
для типа. Первый метод использует функцию-член swap
для выполнения фактической работы и имеет специализацию swap
, которая делегирует ей:
class T {
public:
// ....
void swap(T&) { ... }
};
void swap(T& a, T& b) { a.swap(b); }
Это довольно распространено в стандартной библиотеке; std::vector
, например, имеет подкачку, реализованную таким образом. Если у вас есть функция-член swap
, вы можете просто вызвать ее непосредственно из оператора присваивания и избежать проблем с поиском функций.
Другой способ - объявить swap
как функцию друга и выполнить всю работу:
class T {
// ....
friend void swap(T& a, T& b);
};
void swap(T& a, T& b) { ... }
Я предпочитаю второй, поскольку swap()
обычно не является неотъемлемой частью интерфейса класса; это кажется более подходящим как свободная функция. Однако это вопрос вкуса.
Наличие оптимизированного swap
для типа является распространенным методом достижения некоторых преимуществ ссылок rvalue в С++ 0x, поэтому хорошая идея в целом, если класс может воспользоваться ею, и вам действительно нужно производительность.
Ответ 2
С вашей версией конструктора копирования члены сначала создаются по умолчанию, а затем назначаются.
С интегральными типами это не имеет значения, но если у вас есть нетривиальные члены, такие как std::string
, это лишние накладные расходы.
Таким образом, да, в общем, ваш альтернативный конструктор копий лучше, но если у вас есть только интегральные типы в качестве членов, это не имеет особого значения.
Ответ 3
По сути, вы говорите, что у вас есть члены вашего класса, которые не вносят вклад в личность класса. В настоящее время это означает, что это выражается с помощью оператора присваивания для копирования членов класса и последующего сброса тех элементов, которые не должны копироваться. Это оставляет вам оператор присваивания, который несовместим с конструктором копирования.
Намного лучше было бы использовать икону копирования и подкачки и выразить, какие члены не должны копироваться в конструкторе копирования. У вас все еще есть одно место, где выражается поведение "не копировать этот элемент", но теперь ваш оператор присваивания и конструктор копирования согласованы.
class A
{
public:
A() : a(), b(), c(), d() {}
A(const A& other)
: a(other.a)
, b(other.b)
, c() // c isn't copied!
, d(other.d)
A& operator=(const A& other)
{
A tmp(other); // doesn't copy other.c
swap(tmp);
return *this;
}
void Swap(A& other)
{
using std::swap;
swap(a, other.a);
swap(b, other.b);
swap(c, other.c); // see note
swap(d, other.d);
}
private:
// ...
};
Примечание: в функции члена swap
, я поменял элемент c
. Для целей использования в операторе присваивания это сохраняет поведение, соответствующее поведению конструктора копирования: оно повторно инициализирует член c
. Если вы оставите функцию swap
общедоступной или предоставите доступ к ней с помощью бесплатной функции swap
, вы должны убедиться, что это поведение подходит для других видов использования swap.
Ответ 4
Лично я считаю, что сломанный оператор присваивания является убийцей. Я всегда говорю, что люди должны читать документацию, а не делать то, что она им говорит, но даже так просто написать задание, не думая об этом, или использовать шаблон, который требует назначения типа. Там есть причина для неготовности идиомы: если operator=
не будет работать, это слишком опасно, чтобы оставить его доступным.
Если я правильно помню, С++ 0x позволит вам сделать это:
private:
A &operator=(const A &) = default;
Тогда, по крайней мере, это только сам класс, который может использовать прерывистый оператор присваивания по умолчанию, и вы надеетесь, что в этом ограниченном контексте легче быть осторожным.
Ответ 5
Я бы назвал это плохой формой, а не потому, что вы дважды назначили все свои объекты, но потому, что по моему опыту часто бывает плохой способ полагаться на оператор-конструктор/присваивание по умолчанию для определенного набора функций. Поскольку они нигде не находятся в источнике, трудно сказать, что поведение, которое вы хотите, зависит от их поведения. Например, что, если кто-то через год хочет добавить вектор строк в ваш класс? У вас больше нет простых старых типов данных, и разработчику будет очень сложно знать, что они нарушают вещи.
Я думаю, что, красивый, как DRY, создание тонких неописанных требований намного хуже с точки зрения обслуживания. Даже повторение себя, так же плохо, как это, меньше зла.
Ответ 6
Я думаю, что лучший способ - не реализовать конструктор копирования, если поведение тривиально (в вашем случае оно кажется сломанным: по крайней мере назначение и копирование должны иметь сходную семантику, но ваш код предполагает, что этого не будет - но тогда я полагаю, что это надуманный пример). Код, созданный для вас, не может быть неправильным.
Если вам нужно реализовать эти методы, скорее всего, класс может сделать с помощью метода быстрой замены и, таким образом, сможет повторно использовать конструктор копирования для реализации оператора присваивания.
Если вам по какой-то причине необходимо предоставить конструктор неполной копии по умолчанию, тогда С++ 0X имеет
X(const X&) = default;
Но я не думаю, что есть идиома для странной семантики. В этом случае использование присваивания вместо инициализации дешево (поскольку оставляя ints uninitialized ничего не стоит), так что вы можете так же хорошо сделать это, как это.