*** ВНИМАНИЕ: Блог переехал на другой адрес - demin.ws ***

вторник, 27 января 2009 г.

Защита объектов от случайного копирования в С++

Внимательный читатель наверняка заметил в посте про реализацию параллельных потоков на С++ следующий фрагмент кода:
class Thread {
public:
...
private:
...
// Защита от случайного копирования объекта в C++
Thread(const Thread&);
void operator=(const Thread&);
};
Это определения конструктора копирования и перегруженого оператора присваивания. Причем непосредственно реализаций этих функций нигде нет, только определения. Вопрос: для чего все это?
Давайте разберемся с назначением этих функций. Их прямая задача уметь копировать объект данного класса. А что произойдет, если вы по какой-то причине не определили конструктор копирования или оператор присваивания (может просто забыли), а пользователь вашего класса решил скопировать объект, возможно даже неосознанно? В этом случае компилятор сам определит конструктор копирования по умолчанию, который будет тупо копировать объект байт за байтом без учета смысла копируемых данных. И вам крупно повезет, если все члены-данные вашего класса являются либо базовыми типами (int, long, char и т.д.), либо имеют корректные конструкторы копирования и операторы присваивания. В этом случае все будет хорошо — базовые типы компилятор умеет копировать правильно, а сложные типы скопируют себя сами через их конструкторы копирования. А представьте, что вы внутри своего класса создаете объекты динамически в куче и храните в классе только указатели на них. Указатеть — это базовый тип, и компилятор его нормально скопирует. А вот данные, на которые этот указатель указывает он копировать не будет. В итоге два объекта (старый-оригинал и новый-копия) будут ссылаться на один кусок памяти в куче. Теперь понятно, что при попытке освобождения этой памяти в деструкторе (если вы не забыли этого сделать ;-) кто-то из этих двух объектов попытается освободить уже освобожденную память. Вероятность аварийного завершения программы в этом случае крайне высока, а поиск подобных ошибок может быть крайне долгим и мучительным.
Отсюда мораль: если для вашего класса не заданы конструктор копирования и оператор присваивания (они вам не нужны по смыслу), сделайте им пустые объявления в разделе закрытом разделе (private). Тогда попытка скопировать этот объект сразу приведет к ошибке при компиляции. Во-первых, объявления являются закрытыми (private), и сторонний пользователь вашего класса получит ошибку доступа к закрытым данным класса. Во-вторых, у этих функций нет тел, а значит вы сами не выстрелите себе в ногу, попытавшись случайно скопировать объект данного класса в нем же самом (тут вам private уже не помеха), если вы на это не рассчитывали при проектирование класса.

Лично я делаю так. У меня есть следующий файл:

ctorguard.h:
#ifndef _EXT_CTOR_GUARD_H
#define _EXT_CTOR_GUARD_H
#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
TypeName(const TypeName&); \
void operator=(const TypeName&)
#endif
Теперь определение класса будет выглядеть так:
#include "ctorguard.h"
class Thread {
public:
...
private:
...
// Защита от случайного копирования объекта в C++
DISALLOW_COPY_AND_ASSIGN(Thread);
};
Теперь вы надежно предохранены.
Кстати, вдогонку. При реализации оператора присваивания надо обязательно проверять — не пытаетесь ли вы копировать объект сам в себя, то есть, не является ли источник копирования самими объектом куда идет копирование. Если это произойдет, вы легко можете получить переполнения стека как самый вероятный исход из-за бесконечного вызова оператора присваивания себя самим.

2 комментария:

  1. Можно воспользоваться наследованием boost::non_copyable

    ОтветитьУдалить
  2. ionial: Конечно можно. boost хорош, но многие "родные" компиляторы типа AIX'вого или HP-UX'ного STL-то понимают по-разному, а про boost вообще говорить не приходится.

    ОтветитьУдалить