Варианты реализации конструктора копирования и оператора присваивания -

Недавно я вновь посетил конструктор копирования, оператор присваивания, идентификатор подкачки копии, который можно увидеть здесь: что такое подстановка копирования и замены? и много других мест -

Ссылка "Выше" - отличный пост, но у меня все еще оставалось еще несколько вопросов. На эти вопросы можно найти ответы во многих местах, на stackru и на многих других сайтах, но я не видел большой последовательности -

1 - Если у вас есть try - catch вокруг областей, где мы выделяем новую память для глубокой копии в конструкторе копирования? (Я видел это в обоих направлениях)

2 - Что касается наследования как для конструктора копирования, так и для оператора присваивания, когда следует вызывать функции базового класса и когда эти функции должны быть виртуальными?

3 - есть std::copy лучший способ для дублирования памяти в конструкторе копирования? Я видел это с memcpy и видел, как другие говорят memcpy худшая вещь на земле.


Рассмотрим пример ниже (спасибо за все отзывы), он вызвал несколько дополнительных вопросов:

4 - Должны ли мы проверять себя? Если да где

5 - Вопрос не по теме, но я видел, что подкачка используется как: std::copy(Other.Data,Other.Data + size,Data); должно ли это быть: std::copy(Other.Data,Other.Data + (size-1),Data); если swap переходит от "First to Last", а 0-й элемент - Other.Data?

6 - Почему закомментированный конструктор не работает (мне пришлось изменить размер на mysize) - предполагается, что это означает, что независимо от порядка, в котором я их пишу, конструктор всегда будет сначала вызывать элемент размещения?

7 - Есть еще комментарии по поводу моей реализации? Я знаю, что код бесполезен, но я просто пытаюсь проиллюстрировать это.

class TBar
{

    public:

    //Swap Function        
    void swap(TBar &One, TBar &Two)
    {
            std::swap(One.b,Two.b);
            std::swap(One.a,Two.a);
    }

    int a;
    int *b;


    TBar& operator=(TBar Other)
    {
            swap(Other,*this);
            return (*this);
    }

    TBar() : a(0), b(new int) {}                //We Always Allocate the int 

    TBar(TBar const &Other) : a(Other.a), b(new int) 
    {
            std::copy(Other.b,Other.b,b);
            *b = 22;                                                //Just to have something
    }

    virtual ~TBar() { delete b;}
};

class TSuperFoo : public TBar
{
    public:

    int* Data;
    int size;

    //Swap Function for copy swap 
    void swap (TSuperFoo &One, TSuperFoo &Two)
    {
            std::swap(static_cast<TBar&>(One),static_cast<TBar&>(Two));
            std::swap(One.Data,Two.Data);
            std::swap(One.size,Two.size);
    }

    //Default Constructor
    TSuperFoo(int mysize = 5) : TBar(), size(mysize), Data(new int[mysize]) {}
    //TSuperFoo(int mysize = 5) : TBar(), size(mysize), Data(new int[size]) {}                *1

    //Copy Constructor
    TSuperFoo(TSuperFoo const &Other) : TBar(Other), size(Other.size), Data(new int[Other.size])        // I need [Other.size]! not sizw
    {
            std::copy(Other.Data,Other.Data + size,Data);        // Should this be (size-1) if std::copy is First -> Last? *2
    }

    //Assignment Operator
    TSuperFoo& operator=(TSuperFoo Other)
    {
            swap(Other,(*this));
            return (*this);
    }

    ~TSuperFoo() { delete[] Data;}

};

4 ответа

Решение
  1. Если вы выделяете память, вам нужно убедиться, что она освобождена в случае возникновения исключения. Вы можете сделать это с явным try/catchили вы можете использовать умный указатель, такой как std::unique_ptr удерживать память, которая затем будет автоматически удалена, когда умный указатель будет уничтожен при размотке стека.

  2. Тебе очень редко нужна virtual оператор присваивания. Вызовите конструктор копирования базового класса в списке инициализации члена и оператор присваивания базового класса первым в производном операторе присваивания, если вы выполняете присваивание по элементам - если вы выполняете копирование / своп, то вам не нужно вызывать Назначение базового класса в вашем производном операторе присваивания, при условии, что копирование и обмен реализованы правильно.

  3. std::copy работает с объектами и будет правильно вызывать конструкторы копирования. Если у вас есть простые объекты POD, то memcpy будет работать так же хорошо. Я бы пошел на std::copy хотя в большинстве случаев это должно быть оптимизировано для memcpy в любом случае, для POD, и это исключает возможность ошибок, если вы добавите конструктор копирования позже.

[Обновления для обновленного вопроса]

  1. С копией / свопом, как написано, нет необходимости проверять самоназначение, и, действительно, нет способа сделать это - к тому времени, когда вы вводите оператор присваивания other является копией, и у вас нет возможности узнать, что это был за исходный объект. Это просто означает, что самостоятельное назначение все равно будет выполнять копирование / обмен.

  2. std::copy принимает в качестве входных данных пару итераторов (первый, первый + размер). Это допускает пустые диапазоны и является тем же, что и каждый алгоритм на основе диапазонов в стандартной библиотеке.

  3. Закомментированный конструктор не работает, потому что элементы инициализируются в том порядке, в котором они были объявлены, независимо от порядка в списке инициализаторов элементов. Как следствие, Data всегда инициализируется первым. Если инициализация зависит от size тогда он получит значение Дафф, так как size еще не был инициализирован. Если вы поменяете местами декларации size а также data тогда этот конструктор будет работать нормально. Хорошие компиляторы будут предупреждать о порядке инициализации членов, не совпадающем с порядком объявлений.

1 - Должны ли вы попытаться обойти те области, где мы выделяем новую память для глубокой копии в конструкторе копирования?

В общем, вы должны поймать исключение, только если вы можете его обработать. Если у вас есть способ локально справиться с нехваткой памяти, то перехватите его; в противном случае, отпустите.

Вы, конечно, не должны нормально возвращаться из конструктора, если конструкция не удалась - это привело бы к тому, что вызывающая сторона оставалась с недопустимым объектом, и невозможно было бы узнать, что он недействителен.

2 - Что касается наследования как для конструктора копирования, так и для оператора присваивания, когда следует вызывать функции базового класса и когда эти функции должны быть виртуальными?

Конструктор не может быть виртуальным, поскольку виртуальные функции могут отправляться только объектом, и до его создания нет объекта. Обычно вы бы не делали операторы присваивания виртуальными; Копируемые и присваиваемые классы обычно рассматриваются как неполиморфные типы "значений".

Обычно вы вызываете конструктор копирования базового класса из списка инициализаторов:

Derived(Derived const & other) : Base(other), <derived members> {}

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

void swap(Derived & a, Derived & b) {
    using namespace std;
    swap(static_cast<Base&>(a), static_cast<Base&>(b));
    // and swap the derived class members too
}
Derived & Derived::operator=(Derived other) {
    swap(*this, other);
    return *this;
}

3 - есть std::copy лучший способ для дублирования памяти в конструкторе копирования? Я видел это с memcopyи видел, как другие говорят memcopy худшая вещь на земле.

Довольно необычно иметь дело с сырой памятью; обычно ваш класс содержит объекты, и часто объекты не могут быть правильно скопированы путем простого копирования их памяти. Вы копируете объекты, используя их конструкторы копирования или операторы присваивания, и std::copy будет использовать оператор присваивания для копирования массива объектов (или, в более общем случае, последовательности объектов).

Если вы действительно хотите, вы можете использовать memcpy копировать объекты и массивы POD (обычные старые данные); но std::copy менее подвержен ошибкам (так как вам не нужно указывать размер объекта), менее хрупок (поскольку он не сломается, если вы измените объекты не на POD) и потенциально быстрее (поскольку размер и выравнивание объекта известно во время компиляции).

  1. Если конструктор того, что вы глубоко копируете, может выбросить что-то, с чем вы можете справиться, продолжайте и поймайте это. Я бы просто позволил распространяться исключениям выделения памяти.
  2. Конструкторы копирования (или любые конструкторы) не могут быть виртуальными. Включите инициализатор базового класса для них. Операторы копирования должны делегироваться базовому классу, даже если они виртуальные.
  3. memcpy() слишком низкий уровень для копирования типов классов в C++ и может привести к неопределенному поведению. Я думаю std::copy обычно лучший выбор.
  1. try-catch можно использовать, когда нужно что-то отменить. В противном случае, просто дайте bad_alloc распространяться на звонящего.

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

  3. std::copy имеет то преимущество, что правильно копирует объекты класса. memcpy довольно ограничен на том, какие типы он может обрабатывать.

Другие вопросы по тегам