Личный опыт разработки ПО

Сборник рецептов

О копировании объектов в C++

комментариев 10

Я регулярно сталкиваюсь с ошибками связанными с невнимательностью или незнанием механизма копирования объектов в C++. Поэтому первое правило:

Не копируйте!

Задайте себе вопрос, действительно ли класс должен поддерживать копирование? Скорее всего это не нужно как по соображениям эффективности (передавать объект по ссылке или указателю менее накладно, чем создавать его копию при передаче по значению), так и просто исходя из здравого смысла – зачем две копии объекта, представляющего к примеру базу данных с пользователями или порт? Поэтому сделайте класс некопируемым. Для этого надо перенести объявления копирующего конструктора и оператора присваивания в защищенную секцию (определять их необязательно):

class Port
{
public:
	Port();
	virtual ~Port();
 
private:
	Port(const Port&);
	Port& operator=(const Port&);
};

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

Но чтобы каждый раз не писать данный код и дать людям ясно понять чего вы хотите, унаследуйте класс от boost::noncopyable:

#include <boost/noncopyable.hpp>
 
class Port
	: private boost::noncopyable
{
public:
	Port();
	virtual ~Port();
};

На этом можно остановиться, но если же вы решили, что класс должен поддерживать копирование, то читайте дальше.

Функции создаваемые компилятором

Компилятор будет стараться вам помочь и если вы не написали конструктор по умолчанию, деструктор, оператор присваивания или копирующий конструктор, создаст их сам. Причем созданы они будут только если они действительно требуются. Рассмотрим простой класс:

class Message
{
private:
	unsigned Id_;
	std::string Text_;
};

При его создании компилятор автоматически создаст конструктор по умолчанию в котором будет вызван конструктор по умолчанию для каждого из членов, но только если член не является встроенным типом (int, double и т.д.) и деструктор:

Message()
	: Text_()
{
}

При копировании:

Message m1;
Message m2;
m1 = m2;

Будет автоматически сгенерирован оператор присваивания, который вызовет оператор присваивания для каждого члена:

Message& operator=(const Message& from)
{
	Id_ = from.Id_;
	Text_ = from.Text_;
	return *this;
}

Здесь я хочу обратить внимание на то, что оператор присваивания возвращает ссылку на *this. Это нужно для того, чтобы запись вида

m1 = m2 = m3;

была законна. Это общепринятое поведение, поэтому если будете писать свой оператор присваивания, постарайтесь придерживаться общепринятой формы.

Копирующий конструктор будет создан если вы напишете:

Message m1;
Message m2(m1);

И как и ожидается, он проинициализирует каждый член конструируемого объекта значением из другого объекта:

Message(const Message& from)
	: Id_(from.Id_)
	, Text_(from.Text_)
{
}

Конструктор, копирующий конструктор и оператор присваивания не будет создан автоматически, если класс содержит константный член или член являющийся ссылкой.

Теперь следующее правило:

Дайте компилятору помочь вам

Не мешайте компилятору написать за вас оператор присваивания и копирующий конструктор – в отличии от нас он не забудет скопировать каждый из членов. Особенно часто программисты забывают при добавлении нового члена в класс обновить также и данные методы. Компилятор обиженный отказом от предложенной помощи не скажет вам, что класс копируется не полностью!

Копируйте полностью

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

class Message
{
public:
	Message()
		: Id_(GetId())
	{
	}
 
	virtual ~Message()
	{
	}
 
	Message(const Message& from)
		: Id_(GetId())
		, Text_(from.Text_)
	{
	}
 
	Message& operator=(const Message& from)
	{
		Id_ = GetId();
		Text_ = from.Text_;
		return *this;
	}
 
private:
	unsigned Id_;
	std::string Text_;
};

Здесь присутствует одна проблема, о которой поговорим позже, а пока все выглядит неплохо. Но давайте создадим класс на основе Message:

class DataMessage
	: public Message
{
public:
	DataMessage()
		: Data_()
	{
	}
 
	~DataMessage()
	{
	}
 
	DataMessage(const DataMessage& from)
		: Data_(from.Data_)
	{
	}
 
	DataMessage& operator=(const DataMessage& from)
	{
		Data_ = from.Data_;
		return *this;
	}
 
private:
	CompressedData Data_;
};

Все отлично, но есть проблема – члены Message больше не копируются, так как новые копирующий конструктор и оператор присваивания скрыли те же методы родительского класса, а значит придется их вызвать явно:

DataMessage(const DataMessage& from)
	: Message(from)
	, Data_(from.Data_)
{
}
 
DataMessage& operator=(const DataMessage& from)
{
	Message::operator=(from);
	Data_ = from.Data_;
	return *this;
}

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

class Customer
{
public:
	Customer(const std::string& firstName, const std::string& secondName);
 
private:
	HashType Hash_;
	std::string FirstName_;
	std::string SecondName_;
};
 
Customer::Customer(const std::string& firstName, const std::string& secondName)
	: FirstName_(firstName)
	, SecondName_(secondName)
	, Hash_(GetHash(FirstName_ + SecondName_))
{
}

Ой! Мы хотим получить хэш от еще не инициализированных строк!

Теперь о проблеме которая осталась – мы не проверяем присваивание самому себе. Следующее правило:

Проверяйте на присваивание самому себе

Конечно вы никогда не напишите

message = message;

А как насчет этого:

Messages[i] = Messages[j];

В любом случае, по закону Мерфи, если уж что-то можно сделать неправильно, то это будет сделано. Просто защитите себя от этого:

DataMessage& operator=(const DataMessage& from)
{
	if (this == &from)
	{
		return *this;
	}
	Message::operator=(from);
	Data_ = from.Data_;
	return *this;
}

Также можно например писать о некорректном присваивании в лог ошибок.

Теперь можно поговорить о устранении дублирования кода в операторе присваивания и копирующем конструкторе. Буду краток:

Не пытайтесь реализовать оператор присваивания через копирующий конструктор или наоборот

У вас ничего не выйдет. И хотя делают они очень много похожего, но по своей сути они слишком разные чтобы их удалось скрестить. В первом случае вы будете создавать объект который уже существует, во втором присваивать что-то еще не созданному объекту. Единственное, что можно сделать – это выделить общий код в отдельную функцию вызываемую обоими методами:

class Sensor
{
public:
	Sensor(Controller* controller)
		: Controller_(controller)
	{
		Init();
	}
 
	~Sensor()
	{
		Shutdown();
	}
 
	Sensor(const Sensor& from)
		: Controller_(from.Controller_)
	{
		Init();
	}
 
	Sensor& operator=(const Sensor& from)
	{
		if (this == &from)
		{
			return *this;
		}
		Shutdown();
		Controller_ = from.Controller_;
		Init();
		return *this;
	}
 
private:
	void Init()
	{
		// Тут может быть что угодно
		Controller_->Register(this);
	}
 
	void Shutdown()
	{
		// Тут может быть что угодно
		Controller_->Unregister(this);
	}
 
private:
	Controller* Controller_;
};

Но и здесь зоркий глаз может обнаружить проблему. Последнее правило:

Напишите безопасный в смысле исключений оператор присваивания

Дело в том, что в предыдущем примере, если из Init в операторе присваивания вылетит исключение, объект останется в несогласованном состоянии, так как был вызван метод Shutdown, но повторная инициализация проведена не была. Рассмотрение данного вопроса выходит за рамки заметки, поэтому посоветую обратиться к Правилу 29: Стремитесь, чтобы программа была безопасна относительно исключений из книги Эффективное использование C++ 55 верных советов улучшить структуру и код ваших программ Скотта Мэйерса.

23rd Март 2010
0:18

Рубрика: C++

Метки:

10 комментариев к 'О копировании объектов в C++'

Подписаться на комментарии по RSS или TrackBack.

  1. Пример с присваиванием плох наличием двх return.
    DataMessage& operator=(const DataMessage& from)
    {
    if (this == &from)
    {
    return *this;
    }
    Message::operator=(from);
    Data_ = from.Data_;
    return *this;
    }
    Это заставляет компилятора вставлять 2 раза код возврата, того хуже, вызова деструкторов локальных переменных, если они будут.
    Предпочтительный вариант, который можно найти у Майерса:
    DataMessage& operator=(const DataMessage& from)
    {
    if (this != &from)
    {
    Message::operator=(from);
    Data_ = from.Data_;
    }
    return *this;
    }

    ilnar

    30 июля 10 10:42

  2. ilnar, в целом справедливо, но:
    1) Никаких локальных переменных до блока проверки быть не должно по определению
    2) Вопрос вкуса, но мне кажутся наглядней короткие блоки if

  3. еще про запрет копирования, в Qt есть такой простой макрос
    #define Q_DISABLE_COPY(Class) \
    Class(const Class &); \
    Class &operator=(const Class &);

    просто и работает, меньше возьни с ручным описанием

    Vernat

    6 сентября 10 15:59

  4. Очень интересная статья.

    А я всегда пишу в копируемых классах конструктор копирования и оператор присваивания.
    Боюсь, вдруг компилятор неправильно скопирует ;)

    xrnd

    20 января 11 7:04

  5. Работать компилятором — дело неблагодарное :)

  6. Цитата: Для этого надо перенести объявления копирующего конструктора и оператора присваивания в защищенную секцию (определять их необязательно)…
    Лично я склоняюсь к тому, что их нужно ОБЯЗАТЕЛЬНО НЕ определять… Поскольку, не дай Бог внутри класса Port вы присвоите один порт другому и это присваивание прокатит, так как имеется их реализация. Но если реализация отсутствует, ошибка будет отловлена на стадии линковки.

    picania

    2 февраля 11 22:12

  7. Согласен

  8. 2picania: А если добрый дядя из соседнего отдела в своем cpp файле предусмотрит определение и копирующего конструктора и оператора присваивания для Вашего класса? Вобщем это извечный вопрос определять или не определять (только объявлять). Лично я тоже не определяю, но доброго дядю остерегаюсь.

    sba

    18 мая 11 16:17

  9. Отличное решение. Респект автору за учебу. Несколько полезных советов не помешают.

    samsim

    19 ноября 13 1:00

  10. http://www.cplusplus.com/articles/y8hv0pDG/. — обсуждение этого вопроса на ресурсе cpluplus за 2010 год

    Konstantin Burlachenko

    12 января 16 2:57

Оставить комментарий