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

Неплохая ли форма вызова оператора присваивания по умолчанию из конструктора копирования?

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

Является ли плохая форма вызовом оператора присваивания по умолчанию из конструктора копирования?

Оператор присваивания по умолчанию будет хорошо вести себя с обычными старыми данными (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"? */
4b9b3361

Ответ 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 ничего не стоит), так что вы можете так же хорошо сделать это, как это.