Итак, вы решили запретить копирование объектов класса в C++

    SHALL NOT DANCE THEREДовольно часто можно встретить код на C++, в котором у одного или нескольких классов конструктор копирования и оператор присваивания объявлены private и написан комментарий вида «копирование запрещено».

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

    Рассмотрим возможные проблемы.

    Сначала – краткий экскурс, зачем нужен этот прием.

    Если программа пытается скопировать объект класса, компилятор C++ по умолчанию автоматически генерирует конструктор копирования или оператор присваивания, если они не объявлены в классе явно. Автоматически сгенерированный конструктор выполняет почленное копирование, а автоматически сгенерированный оператор присваивания – почленное присваивание.

    Вот простой пример:

    class DoubleDelete {
    public:
       DoubleDelete()
       {
           ptr = new char[100];
       }
       ~DoubleDelete()
       {
           delete[] ptr;
       }
    
    private:
       char* ptr;
    };
    

    В этом коде:
    {
       DoubleDelete first;
       DoubleDelete second( first );
    }
    

    возникнет неопределенное поведение. Будет вызван сгенерированный компилятором конструктор копирования, который скопирует указатель. В результате оба объекта будут хранить указатели с равными адресами. Первым отработает деструктор объекта second, он выполнит delete[], затем будет вызван деструктор объекта first, он попытается выполнить delete[] повторно для того же адреса, и это приведет к неопределенному поведению.

    Решение вроде бы очевидно – реализовать конструктор копирования и оператор присваивания с правильным поведением. Например, при копировании новый объект создает свой массив и копирует в него данные из старого.

    Это не всегда верный путь. Не все объекты по своей сути подлежат копированию.

    Например, объект может хранить дескриптор открытого файла в виде целого числа. Деструктор «закрывает файл» с помощью функции операционной системы. Очевидно, почленное копирование не подойдет. А что должно происходить при копировании? Должен ли файл открываться еще раз? Обычно в этом случае копирование не имеет смысла.

    Другой пример – класс для захвата критической секции при создании объекта такого класса. Какой смысл копировать объект? Секция уже захвачена.

    Краткий экскурс на этом закончен, переходим к попытке изобразить решение.

    Если копирование объекта не имеет смысла, нужно сделать так, чтобы компилятор не смог случайно его выполнить. Для этого обычно делают так:

    // NOT BAD
    class NonCopyable {
    // blahblahblahpublic:
    private:
       // copy and assignment prohibited
       NonCopyable( const NonCopyable& );
       void NonCopyable::operator=( const NonCopyable& );
    };
    

    или так:

    // FAIL
    class NonCopyable {
    // blahblahblahpublic:
    private:
       // copy and assignment prohibited
       NonCopyable( const NonCopyable& ) { assert( false ); }
       void NonCopyable::operator=( const NonCopyable& ) { assert( false ); }
    };
    

    или так:

    // EPIC FAIL
    class NonCopyable {
    // blahblahblahpublic:
    private:
       // copy and assignment prohibited
       NonCopyable( const NonCopyable& ) {}
       void NonCopyable::operator=( const NonCopyable& ) {}
    };
    

    Все три способа встречаются в реальном коде.

    Казалось бы, чем второй и третий варианты отличаются от первого? Модификатор private в любом случае не даст вызвать копирование.

    КРАЙНЕ НЕОЖИДАННО…

    Функции-члены того же класса могут вызывать конструктор копирования и оператор присваивания, даже если те объявлены private. И «друзья» класса (friend) тоже могут. Никто не мешает написать в коде что-нибудь такое:

    NonCopyable NonCopyable::SomeMethod()
    {
        // blahblahblah
        return *this;
    }
    

    или такое:

    void NonCopyable::SomeOtherMehod()
    {
        callSomething( *this );
    }
    

    Теперь налицо разница между первым вариантом и остальными.

    Первый вариант (нет реализации) приведет к ошибке во время компоновки программы. Сообщение об ошибке не самое понятное, но, по крайней мере, надежное.

    Во втором варианте будет срабатывать assert… при условии, что управление пройдет через этот код. Здесь многое зависит от того, насколько часто этот код вызывается, в частности, от покрытия кода тестами. Может быть, вы заметите проблему при первом же запуске, может быть – очень нескоро.

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

    Ожидаемое возражение – раз конструктор копирования и оператор присваивания объявлены, но не определены, их можно по ошибке или злонамеренно определить где угодно в коде. Эта проблема решается очень просто.

    От ошибки помогает комментарий вида «запрещенные операции» или, если есть сомнения, «запрещенные операции, не определять под страхом увольнения». От злого умысла не поможет ничто – в C++ никто не мешает взять адрес объекта, привести его к типу char* и побайтово перезаписать объект как угодно.

    В C++0x есть ключевое слово delete:

    // C++0x OPTIMAL
    class NonCopyable {
    private:
       // copy and assignment not allowed
       NonCopyable( const NonCopyable& ) = delete;
       void operator=( const NonCopyable& ) = delete;
       // superior developers wanted – www.abbyy.ru/vacancy
    };

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

    Вариант «объявить и не определять» доступен и ранее C++0x, его, в частности, использует boost::noncopyable. Вариант наследоваться от boost::noncopyable или аналогичного своего класса тоже достаточно надежен и доступен в любой версии.

    Внимательный читатель наверняка обратил внимание, что во всех примерах выше оператор присваивания возвращает void, а не ссылку на тот же класс. Это сделано специально, чтобы конструкции вида

    first = second = third;
    

    вызывали ошибку компиляции в С++03.

    Так небольшие улучшения кода иногда помогают избежать премии Дарвина.

    Дмитрий Мещеряков,
    департамент продуктов для разработчиков
    ABBYY
    253,00
    Решения для интеллектуальной обработки информации
    Поделиться публикацией

    Комментарии 45

      +2
      бустовский вариант:
      namespace noncopyable_  // protection from unintended ADL
      {
        class noncopyable
        {
         protected:
            noncopyable() {}
            ~noncopyable() {}
         private:  // emphasize the following members are private
            noncopyable( const noncopyable& );
            const noncopyable& operator=( const noncopyable& );
        };
      }
      
        +10
        Понравился код с комментарием:
        // superior developers wanted – www.abbyy.ru/vacancy
          +2
          Блин, всегда писал EPIC FAIL код.
          Ну что ж, хорошо что на плюсах я пишу только для себя :)
            0
            Другой пример – класс-скобка для захвата критической секции


            Если не секрет, откуда термин «класс-скобка»?
              0
              После тщательного гугления приходится прийти к выводу, что «термин» порожден мозгом автора. Из текста изведен.
                0
                Возможно, имелся в виду класс-обертка, «wrapper class».
                  0
                  Нет, речь именно о классе, объекты которого создаются на стеке. К объектам классов-оберток такого требования нет — можно поле другого класса, например, сделать.
                    0
                    Я думаю речь о функторе
                      0
                      Мне кажется, речь идет о RAII-классе
                        0
                        Это он, да, но есть ли для него русскоязычный термин?
                          0
                          Согласно википедии, сам метод переводится «Получение ресурса есть инициализация». Не встречал общепризнанного перевода RAII-класса, могу предложить лишь свой: «владелец ресурса», но он мне нравится меньше, чем «RAII-класс».
                +1
                Кому любопытно узнать пару фактов об автогенерируемом конструкторе копирования, тут вот когда-то накидал небольшую почеркушку: Автогенерируемый конструктор копирования в С++. Когда-то вот в стандарте копался из любопытства и любви к деталям…
                  0
                  Почитал ваш пост по ссылке и я видать сильно туплю, но я не понимаю почему

                  struct A {
                    A() {};
                    A(A&) {};
                  };
                  
                  struct B : public A {
                    B() {};
                  };
                  
                  
                  int main() {
                    const B b1;
                    B b2 = b1;
                  }
                  

                  Вызывает ошибку.

                  Ведь вы же сами написали
                  «Неявно декларированный конструктор копирования для класса X имеет форму X::X (const X&) в том случае, если:
                  - каждый прямой или виртуальный базовый класс B класса X имеет копирующий конструктор, чей первый параметр - const B& или const volatile B& , и
                  - у каждого нестатического члена класса X, который имеет тип M (или массив таковых) имеет конструктор копирования первый аргументом которого является const M& или const volatile M&.
                  !!!
                  Во всех остальных случаях он имеет форму X::X (X&).
                  !!!
                  Конструктор копирования всегда является inline public членом класса»


                  Так что же мешает компилятору вызвать конструктор копирования
                  B(B&){};
                  


                  Если он сам же его и сгенерировал?
                  Либо вы не правильно интерпретировал стандарт, либо это FAIL в стандарте.
                    0
                    Охтыж демонический разврат
                    const B b1;
                    


                    сори :(
                      0
                      имхо, никакого разврата тут нет. вы же пишете
                      const int i = 5
                      , чем эта ситуация? Класс B может быть чем угодно. Если представить, что это Complex, то становится немного логичнее?
                        0
                        это я к тому что понял свою ошибку :) почему вот это конструктор копирования B(B&){}; не подходит.
                          0
                          а, ну бывает)
                          0
                          опечатка:
                          «чем эта ситуация?» = «чем эта ситуация отличается?»
                        0
                        никакого фейла в стандарте, все логично. Если вы явно сделали аргумент копирующего конструктора не константным (изменямым, это очень важно), то полагается, что вы будете его менять во время копирования (возможно это вам действительно необходимо, возможно вы ошиблись, возможно вас просто переклинило в тот момент). Если вы хотите менять копируемый объект во время копирования, то туда нельзя передавать const-объект. Где тут фейл? все логично. Компилятор не есть ясновидящий, он не знает чего вы хотите (и действительно ли вы хотите чего-то, а не просто напарили), поэтому ему только и остается следовать правилам.
                      0
                      Функции-члены того же класса могут вызывать конструктор копирования и оператор присваивания, даже если те объявлены private. И «друзья» класса (friend) тоже могут.

                      Обычный паттерн — это как в бусте — объявить простой trait класс NonCopyable и от него наследоваться.
                      В таком случае никакие френды или функции в классах наследниках не смогут скопировать класс.

                      На самом деле еще лучше иметь два класса — NonCopyable и NonAssignable (запрещает только присваивание)… и до кучи NonInheritable (но этот уже использует несколько спорный трюк, и его не все платформы поддерживают, AFAIK).
                        +1
                        В смысле, я на C++0x бочку не качу… а то минусовать начнете, как предателя :-)
                        C++0x forever!
                        0
                        А в чем отличие предложенного метода определения вручную конструктора копирования и оператора = от ключевого слова explicit?
                          0
                          При использовании ключевого слова explicit можно, не очень подумав, написать код, который явно вызывает копирование, этот код скомпилируется.
                            0
                            Понятно.
                            Спасибо.
                          –3
                          Картинка напомнила:

                          Всякая ОС — это минное поле:

                          Unix — единственное в мире поле, заминированное с соблюдением всех обычаев войны, из-за своей уникальности приватизировано и обнесено бетонным забором.

                          DOS — минное поле с табличкой “Миннет” (орфография оригинала).

                          Linux — минное поле, утыканное табличками “ACHTUNG MINEN!” через каждый метр

                          Red Hat Linux — минное поле, часть табличек на котором заменена знаками типа “череп и кости”

                          ASP Linux — минное поле со знаками и табличками на русском языке

                          Red Flag Linux — минное поле, огражденное красными флажками и табличками с двумя иероглифами “мин нет”.
                          (Вышеупомянутые поля разминируют саперы-добровольцы. Обезвреживание мин проводится на открытой местности. С каждым днем их запасы тают, но пополняются и списки погибших саперов, которые периодически вывешивают на табличках)

                          FreeBSD — открыто заминированный оазис.

                          OpenBSD — открыто заминированный остров.

                          NetBSD — открыто заминированная поляна в лесу.
                          (Разминируются на месте в ночное время ветеранами-волонтерами)

                          Windows 95 — стадион на минном поле.

                          Windows 98 — школьный стадион на минном поле.

                          Windows Me — стадион спецшколы для умственно отсталых на минном поле.

                          Windows XP Home Edition — лагерь беженцев посреди минного поля.

                          Windows XP Professional — лагерь саперов посреди минного поля.

                          Windows 2000 — лагерь саперов, которые знали о том, что они находятся на минном поле.

                          Windows NT 4 — лагерь саперов на заминированном острове.

                          Windows Server 2003 — склад ГСМ на минном поле.

                          Windows Longhorn — кампания по принудительному минированию личных дачных участков во имя безопасности.
                          (На данных полях все мины тщательно замаскированы и присыпаны землей; за вынос мин с поля расстрел; обнаружение мин с последующим докладом расценивается как попытка посеять панику среди мирного населения; взрывы мин с человеческими жертвами официально считаются провокациями террористов; погибших хоронят в цинковых гробах, вскрытие которых запрещено)

                          Nowell Netware — заминированная железная дорога.

                          BeOS — заминированное болото.

                          Lindows — заминированный туннель под Ла-Маншем.

                          MacOS — минное поле на Луне.

                          MacOS X — минное поле на Луне с табличками для случайных прохожих.
                            0
                            > Автоматически сгенерированный конструктор выполняет
                            > почленное копирование
                            Стоп. Все- таки почленное или побитовое? Что будет, например, если соеди членов класса есть string или какой-нибудь другой объект — он будет скопирован с использованием его собственного конструктора копирования, или все же будет скопирован побитово (и в итоге все сломается)?

                            Т.е. eсли у меня есть простейший класс-структура, внутри которого члены — все std:string и int, то нужно ли мне заморачиваться с конструктором копирования? Я всю жизнь писал для них конструктор копирования (или глушил его через private)…
                              +3
                              Почленное копирование, не побитовое. Так что зря вы всю жизнь писали пользовательские конструкторы.
                                0
                                Ну так в этом случае ситуаций, когда нужно запрещать копирование из-за боязни получить неопределенное поведение, довольно мало. (Почленное копирование же рекурсивно.) Они сводятся к примерам в этой статье — указатели и ссылки на внешние ресурсы, освобождаемые в деструкторе. Если везде использовать shared_ptr (обойдем пока стороной вопрос производительности — в большинстве случаев имеет место случай экономии на спичках), то это уже поборет значительную часть проблем. А если еще и все свести к идеологии «выделение ресурса есть инициализация», то не будет ни деструкторов, ни, соответственно, проблем.

                                Кстати, как iostream относится к копированию? Там есть счетчик ссылок, препятствующий преждевременному закрытию файла?
                                  0
                                  Есть класс A, в котором объявлен член класса shared_ptr m_b.
                                  Вы создали экземпляр класса A, затем скопировали.
                                  Теперь обе копии класса A будут внутри ссылаться на общий объект класса B.
                                  Это чревато последствиями, если в классе B есть функции модификации содержимого.
                                  А в много поточной среде придется еще и синхронизацию доступа к общим данным прикручивать.
                                  shared_ptr здесь не панацея.
                                    0
                                    Странно, я никаких тегов для жирного шрифта не добавлял, видать парсер хабра заглючил…
                                      +1
                                      Вы написали
                                       shared_ptr<B>

                                      Это был тег :)
                                        0
                                        Да я потом уже догадался :)
                                        Извиняюсь, забыл нажать «Предпросмотр»…
                                      0
                                      Ну раз вы решили не владеть объектом, а просто на него ссылаться, то уж будьте готовы к тому, что кто-то поменяет его без вашего ведома. И нет разницы, будет ли это объект того же класса А, или кто-то ещё.
                                        0
                                        Автор предыдущего комментария, на который я ответил, считает, что если он будет бездумно все члены в классе объявлять через shared_ptr, то это решит проблему конструктора копирования по умолчанию.

                                        Я же утверждаю, что shared_ptr в таком случае не только не реши проблему, но и создаст новую. Применение shared_ptr кардинальным образом поменяет поведение класса.
                                        Применять именно shared_ptr для этой цели не правильно, здесь нужен умный указатель другого рода, который, к примеру, будет производить копирование (клонирование) указывающего объекта.

                                        Класс с разрешенным конструктором копирования, содержащий внутри shared_ptr может нарушать важный принцип проектирования классов-значений — одна копия объекта класса не должна влиять на другую копию.
                                    +1
                                    Почленное копирование, не побитовое.

                                    Копирование примитивных типов побитовое, это частный случай. Большинство проблем в описанном сценарии возникает из-за почленного копирования указателей и дескрипторов, которые обычно примитивных типов. Отсюда и ложное противопоставление «глубокого» и побитового копирования.

                                    На самом деле при отсутствии объявленного конструктора копирования (для «глубокого» копирования) выполняется почленное копирование, которое для примитивных типов реализовано как побитовое. Так что на самом деле выбор не между «глубоким» и побитовым копированием, а между «глубоким» и почленным.
                                    0
                                    Жуть то какая… на ночь глядя.
                                      +1
                                      И не говорите. 15 лет пишу на c++ (правда, это не основной мой язык), а такое очевидное заблуждение все время считал истиной. Стыдно жуть как.
                                        0
                                        Так язык к этому располагает. Как только нужно что-то чуть более сложное, чем простое копирование, нужно ручками всё остальное почленно скопировать… Возможно, оттуда и иллюзия.
                                    +2
                                    Интересно, что из-за RVO MSVC легко «копирует» возвращаемый из функции объект.

                                    То есть, допустим у нас есть класс Foo и у него запрещено копирование.
                                    И есть метод Foo MakeFoo { return Foo(1); } — ну написали, забыв, что у нас запрещено копирование — компилятор должен такие вещи помнить.
                                    Так вот студия легко скомпилирует код Foo x = MakeFoo(), ибо копирования тут нет.
                                    А вот в гцц этот код не соберется и тут-то мы замечаем, что у нас проблемы с переносимостью и нужно переделывать через void MakeFoo(Foo* result)…
                                      0
                                      Замечательно. Даже запрос в MS Connect есть.
                                        0
                                        Как показывает опыт вот этого поста, активное плюсование запросов в MS Connect заметно способствует скорому исправлению.
                                          0
                                          А, да пофиг уже. RVO отмирает как класс. Move semantics же.

                                          Впрочем, rvo может быть полезен для древнего кода, в который никто не будет добавлять move semantics, да.
                                        0
                                        Теперь надо статью про то, как таки сделать правильное strong-exception-safe копирование :)
                                          0
                                          Ожидал какого-то подвоха, но оказалось все скучно примитивно и основная рекомендация делать так, как я использую у себя в проектах => private и без реализации.
                                          Обычно в таких случая пишут что статья для новичков.
                                          Хотя конечно для них это весьма полезно.
                                            0
                                            А может быть есть способ при такой реализации
                                            // NOT BAD
                                            class NonCopyable {
                                            // blahblahblahpublic:
                                            private:
                                               // copy and assignment prohibited
                                               NonCopyable( const NonCopyable& );
                                               void NonCopyable::operator=( const NonCopyable& );
                                            };
                                            


                                            заменить текст ошибки компилятора (или снабдить его дополнительным варнингом) о том, что это не просто забыли дописать тело, а что это именно запрет копирования объектов этого класса.

                                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                            Самое читаемое