В c ++, когда классы содержат динамически размещаемые данные, обычно разумно явно указывать конструктор копирования, operator = и деструктор. Но активность этих специальных методов частично совпадает. Точнее, оператор = обычно сначала уничтожает, а затем копирует, как в конструкторе копирования.
Мой вопрос заключается в том, как написать это наилучшим образом без повторения тех же строк кода и без необходимости в процессоре выполнять ненужную работу (например, ненужное копирование).
Я обычно получаю два метода помощи. Один для строительства и один для уничтожения. Первый вызывается из конструктора копирования и оператора =. Второй используется деструктором и оператором =.
Вот пример кода:
template <class T>
class MyClass
{
private:
// Data members
int count;
T* data; // Some of them are dynamicly allocated
void construct(const MyClass& myClass)
{
// Code which does deep copy
this->count = myClass.count;
data = new T[count];
try
{
for (int i = 0; i < count; i++)
data[i] = myClass.data[i];
}
catch (...)
{
delete[] data;
throw;
}
}
void destruct()
{
// Dealocate all dynamicly allocated data members
delete[] data;
}
public: MyClass(int count) : count(count)
{
data = new T[count];
}
MyClass(const MyClass& myClass)
{
construct(myClass);
}
MyClass& operator = (const MyClass& myClass)
{
if (this != &myClass)
{
destruct();
construct(myClass);
}
return *this;
}
~MyClass()
{
destruct();
}
};
Это даже правильно?
И это хорошая привычка разбивать код таким образом?
Один начальный комментарий: operator=
делает не начать с
разрушая, но путем строительства. В противном случае он покинет
объект в недопустимом состоянии, если конструкция заканчивается через
исключение. Ваш код неверен из-за этого. (Обратите внимание, что
необходимость проверки на самостоятельное назначение обычно является признаком того, что
оператор присваивания не правильный.)
Классическое решение для этого — идиома обмена: вы
добавить функцию-член swap:
void MyClass:swap( MyClass& other )
{
std::swap( count, other.count );
std::swap( data, other.data );
}
который гарантированно не бросать. (Здесь он просто меняет
и указатель, ни один из которых не может бросить.) Тогда вы
реализовать оператор присваивания как:
MyClass& MyClass<T>::operator=( MyClass const& other )
{
MyClass tmp( other );
swap( tmp );
return *this;
}
Это просто и понятно, но любое решение, в котором
все операции, которые могут потерпеть неудачу, заканчиваются перед началом
изменение данных приемлемо. Для простого случая, как ваш
код, например:
MyClass& MyClass<T>::operator=( MyClass const& other )
{
T* newData = cloneData( other.data, other.count );
delete data;
count = other.count;
data = newData;
return *this;
}
(где cloneData
является функцией-членом, которая делает большую часть того, что
ваш construct
делает, но возвращает указатель, и не делает
изменить что-либо в this
).
РЕДАКТИРОВАТЬ:
Не имеет прямого отношения к вашему первоначальному вопросу, но обычно
такие случаи вы делаете не хочу сделать new T[count]
в
cloneData
(или же construct
или что угодно). Это конструирует все
из T
с конструктором по умолчанию, а затем назначает их.
Идиоматический способ сделать это что-то вроде:
T*
MyClass<T>::cloneData( T const* other, int count )
{
// ATTENTION! the type is a lie, at least for the moment!
T* results = static_cast<T*>( operator new( count * sizeof(T) ) );
int i = 0;
try {
while ( i != count ) {
new (results + i) T( other[i] );
++ i;
}
} catch (...) {
while ( i != 0 ) {
-- i;
results[i].~T();
}
throw;
}
return results;
}
Чаще всего это будет сделано с помощью отдельного (частного) менеджера
учебный класс:
// Inside MyClass, private:
struct Data
{
T* data;
int count;
Data( int count )
: data( static_cast<T*>( operator new( count * sizeof(T) ) )
, count( 0 )
{
}
~Data()
{
while ( count != 0 ) {
-- count;
(data + count)->~T();
}
}
void swap( Data& other )
{
std::swap( data, other.data );
std::swap( count, other.count );
}
};
Data data;
// Copy constructor
MyClass( MyClass const& other )
: data( other.data.count )
{
while ( data.count != other.data.count ) {
new (data.data + data.count) T( other.date[data.count] );
++ data.count;
}
}
(и, конечно же, идиома обмена для назначения). Это позволяет
несколько пар счет / данные без риска потери исключения
безопасность.
Я не вижу в этом никакой проблемы, если вы не объявляете конструкции или не уничтожаете виртуальные.
Возможно, вас заинтересует глава 2 в Effective C ++ (Скотт Мейерс), которая полностью посвящена конструкторам, операторам копирования и деструкторам.
Что касается исключений, которые ваш код не обрабатывает должным образом, рассмотрим пункты 10 & 11 в более эффективном C ++ (Скотт Мейерс).
Реализуйте назначение, сначала скопировав правую часть, а затем обменявшись этим. Таким образом, вы также получаете исключительную безопасность, которую ваш код выше не обеспечивает. В противном случае вы можете получить поврежденный контейнер, если construct () завершится с ошибкой после того, как destruct () завершится успешно, потому что указатель на член ссылается на некоторые освобожденные данные, а при уничтожении будет снова освобожден, что приведет к неопределенному поведению.
foo&
foo::operator=(foo const& rhs)
{
using std::swap;
foo tmp(rhs);
swap(*this, tmp);
return *this;
}