Варианты реализации конструктора копирования и оператора присваивания -
Недавно я вновь посетил конструктор копирования, оператор присваивания, идентификатор подкачки копии, который можно увидеть здесь: что такое подстановка копирования и замены? и много других мест -
Ссылка "Выше" - отличный пост, но у меня все еще оставалось еще несколько вопросов. На эти вопросы можно найти ответы во многих местах, на 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 ответа
Если вы выделяете память, вам нужно убедиться, что она освобождена в случае возникновения исключения. Вы можете сделать это с явным
try
/catch
или вы можете использовать умный указатель, такой какstd::unique_ptr
удерживать память, которая затем будет автоматически удалена, когда умный указатель будет уничтожен при размотке стека.Тебе очень редко нужна
virtual
оператор присваивания. Вызовите конструктор копирования базового класса в списке инициализации члена и оператор присваивания базового класса первым в производном операторе присваивания, если вы выполняете присваивание по элементам - если вы выполняете копирование / своп, то вам не нужно вызывать Назначение базового класса в вашем производном операторе присваивания, при условии, что копирование и обмен реализованы правильно.std::copy
работает с объектами и будет правильно вызывать конструкторы копирования. Если у вас есть простые объекты POD, тоmemcpy
будет работать так же хорошо. Я бы пошел наstd::copy
хотя в большинстве случаев это должно быть оптимизировано дляmemcpy
в любом случае, для POD, и это исключает возможность ошибок, если вы добавите конструктор копирования позже.
[Обновления для обновленного вопроса]
С копией / свопом, как написано, нет необходимости проверять самоназначение, и, действительно, нет способа сделать это - к тому времени, когда вы вводите оператор присваивания
other
является копией, и у вас нет возможности узнать, что это был за исходный объект. Это просто означает, что самостоятельное назначение все равно будет выполнять копирование / обмен.std::copy
принимает в качестве входных данных пару итераторов (первый, первый + размер). Это допускает пустые диапазоны и является тем же, что и каждый алгоритм на основе диапазонов в стандартной библиотеке.Закомментированный конструктор не работает, потому что элементы инициализируются в том порядке, в котором они были объявлены, независимо от порядка в списке инициализаторов элементов. Как следствие,
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) и потенциально быстрее (поскольку размер и выравнивание объекта известно во время компиляции).
- Если конструктор того, что вы глубоко копируете, может выбросить что-то, с чем вы можете справиться, продолжайте и поймайте это. Я бы просто позволил распространяться исключениям выделения памяти.
- Конструкторы копирования (или любые конструкторы) не могут быть виртуальными. Включите инициализатор базового класса для них. Операторы копирования должны делегироваться базовому классу, даже если они виртуальные.
memcpy()
слишком низкий уровень для копирования типов классов в C++ и может привести к неопределенному поведению. Я думаюstd::copy
обычно лучший выбор.
try-catch
можно использовать, когда нужно что-то отменить. В противном случае, просто дайтеbad_alloc
распространяться на звонящего.Вызов конструктора копирования или оператора присваивания базового класса является стандартным способом разрешения его копирования. Я никогда не видел варианта использования для оператора виртуального присваивания, поэтому я думаю, что они редки.
std::copy
имеет то преимущество, что правильно копирует объекты класса.memcpy
довольно ограничен на том, какие типы он может обрабатывать.