Что скрывает class Empty {}

    Это заметка о методах, которые C++ создаёт автоматически, даже если вы их не создавали.

    Для кого эта заметка? Надеюсь, что она будет интересна начинающим программистам на С++. А опытным программистам позволит лишний раз освежить и систематизировать свои знания.


    Итак, если вы написали

    class Empty {};

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

    class Empty {
    public:
      // Конструктор без параметров
      Empty();
      // Копирующий конструктор
      Empty(const Empty &);
      // Деструктор
      ~Empty();
      // Оператор присвоения
      Empty& operator=(const Empty &);
      // Оператор получения адреса
      Empty * operator&();
      // Оператор получения адреса константного объекта
      const Empty * operator&() const;
    };

    Надо помнить, что эти функции создаются всегда, и могут приводить к неожиданным результатам.

    К каким это может привести неприятностям?


    Самое неприятное, когда программа работает, но не так как вы хотели. При этом никаких ошибок с точки зрения языка не возникает. Именно к таким неприятностям приводят неявно создаваемые методы.

    Пример первый: Конструкторы


    Рассмотрим класс, который выдаёт сообщения о создании/удалении объектов и поддерживает статический счётчик объектов (для простоты в виде публичного int).

    class CC {
    public:
      CC();
      ~CC();
      static int cnt;
    };

    Реализация тривиальна:

    int CC::cnt(0);
    CC::CC()  { cnt++; cout << "create\n";}
    CC::~CC() { cnt--; cout << "destroy\n";}

    Что будет делать такая программа?

    void f(CC o) {}
    int main() {
      CC o;
      cout << " cnt = " << o.cnt << "\n";
      f(o);
      cout << " cnt = " << o.cnt << "\n";
      f(o);
      cout << " cnt = " << o.cnt << "\n";
      return 0;
    }

    Результат может удивить неподготовленного читателя:

    create
     cnt = 1
    destroy
     cnt = 0
    destroy
     cnt = -1
    destroy

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

    Как вы понимаете, это произошло потому, что мы не учли автоматически созданный конструктор копирования, который только копирует, но ничего не печатает и не корректирует счётчик.

    Мы можем исправить эту ситуацию, если сами допишем конструктор копирования

    CC::CC(const CC &) {
      cnt++; cout << "create (copy)\n";
    }

    Теперь мы получим абсолютно разумный результат:

    create
     cnt = 1
    create (copy)
    destroy
     cnt = 1
    create (copy)
    destroy
     cnt = 1
    destroy

    Аналогичные засады возникают при присвоении (operator=); но...

    Пример второй: Получение адреса


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

    CC * operator&();

    но забыли реализовать его двойник, обладающий теми же (или иными?) нетривиальными свойствами:

    const CC * operator&() const;

    Пока ваша программа ограничивается не-константными объектами:

    СС o;
    CC *p;
    p = &o;

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

    Но рано или поздно появится код:

    CC const o;
    CC const *q = &o;

    И метод

    CC * operator&();

    Предательски не сработает (про перегрузку по const я уже писал вот тут).

    Но хватит, наверно, примеров. Смысл у них у всех примерно один и тот-же. Как же избежать всех описанных неприятностей.

    От этих недоразумений очень легко застраховаться!


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

    Тогда программа просто не слинкуется и вы получите вполне разумное сообщение. Я получил такое:

    /var/tmp//ccGQszLd.o(.text+0x314): In function `main':
    : undefined reference to `CC::operator&() const'

    Ваш компилятор может выразиться чуть-чуть иначе.

    Если этот способ кажется вам корявым (у вас есть на то все основания и я с вами солидарен), то можно создать «полноценные» методы, но сделать их приватными.

    Тогда вы тоже получите сообщения об ошибках ещё на этапе компиляции.

    count2.cpp: In function 'int main()':
    count2.cpp:22: error: 'const CC* CC::operator&() const' is private
    count2.cpp:37: error: within this context

    Согласитесь, что это сообщение выглядит как-то… поприличней.

    Ну и третий способ (последний в списке, но не последний по значению) — просто честно реализовать все необходимые методы, не откладывая это дело в долгий ящик :-)
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 65

      0
      Палка о двух концах ;) С одной стороны вы захламляете код и так известной информацией, с другой стороны клиенты класса должны быть вменяемы, потому как эта информация должна даваться на достаточно раннем этапе изучения С++. Я считаю, что писать для «новичков» нет смысла, вы же не учебники пишете, а работающий код. А если вы прошли стадию учебников, то вам нет смысла изголяться. К тому же люди, которые будет сопровождать ваш код обладают не меньшими знаниями. В противном случае, кто их вообще взял на работу? :)
        0
        Нуу… захламление кода весьма условное… не более 6 строчек.
        Допустим, конструкторы и диструкторы у вас все есть и так, тогда «хлам» — это три строчки:
        A operator=(const A &);
        A * operator&();
        const A * operator&() const;
        Мне кажется, это крошечная плата, за избавление от очень неприятных ошибок.
          +1
          Ммм, вопрос простой — а зачем? :) За 15 лет практики ни разу не сталкивался с ошибками такого рода :) Достаточно описать класс и его интерфейс. Ваш класс не обязан быть универсальным, он не обязан следить за всеми ошибками его использования. Например, молоток не виноват, что им мешают кашу, а не забивают гвозди.
          Рассмотрим по порядку.
          Конструктор по умолчанию, конструктор копии и оператор присваивания сейчас присутствуют почти в любом классе, который имеет какие-либо данные, в эпоху расцвета STL и Boost это логично, так как коллекции требуют наличия этих трех функций дял корректной работы. Для классов, которые только выполняют работу над входными данными и не обладают состоянием, вполне подойдут и «компиляторные» версии конструкторов и операторов.
          Если этих функций нет — значит и не нада. А если вы проектируете свой класс для использования в характерных операциях, то вы и так перегрузите их ;)

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

          Суть в том, что везде пихать ненужные определения это глупо, по моему. В конце-концов, С++ это тот язык, где нада бы знать, что делаешь :)
            +7
            > Ваш класс не обязан быть универсальным
            Простите, возможно это ваш не обязан, а мой обязан :-)
            > он не обязан следить за всеми ошибками его использования.
            Какой из вариантов использования является ошибкой?
            — конструирование?
            — конструирование копии?
            — присвоение?
            — взятие адреса?
            Проблема как раз в том, что ошибки нет! Поэтому её так трудно найти.

            Прелесть c++ в том, что он очень многое проверяет на этапе компиляции. Этим можно пользоваться и получать пользу для себя, а можно не пользоваться. Я предлагаю пользоваться.
          +1
          По крайней мере однозначно в декларации будет утверждено — копируется объект или нет.
          Я думаю, это плюс.
            0
            Зачастую это определяется интерфейсом и назначением класса, и определять это однозначно просто нет необходимости. Это всё вопросы проектирования, но никак не реализации. Правильно спроектированный класс говорит сам за себя, и не должен содержать синтаксический мусор.
              0
              В идеально сферической программе в вакууме и assert-ы не нужны, не говоря даже про тесты.

              IMHO, оставлять «неявные» конструктор копии и присваивание можно только в классах, которые ничего не содержат, кроме POD.
                0
                Об этом я и написал выше. Конструктор копии и оператор присваивания нужны почти всегда, в том смысле, что париться об этом нет смысла, потому как они и так будут. А про операторы взятия адреса — это лишний мусор. К тому же в 1-2% случаев объект не должен копироваться, в этом случае синтаксис будет ещё загадочнее.
                  0
                  Да, я читал. Про операторы взятия адреса — согласен. IMHO, стоит трижды подумать прежде чем переопределять такую операцию.

                  Ну а в тех случаях, когда объект не копируется, я отношусь к такому синтаксису как к assert-у. А к assert-ам я отношусь положительно (вероятно, это наследие C-шного прошлого).
          0
          Сейчас мне приходится много писать на «си с крестами», буквально своей задницей выясняя, а как это всё-таки надо делать. Так что спасибо за справку!
            +1
            Пожалуй, вечером я напишу пост про условные операторы для новичков.
              0
              с чего-то надо начинать ,-)
                0
                Пишите :)
                • UFO just landed and posted this here
                  +1
                  operator= возвращает Empty&, а не Empty
                    +1
                    Точно!
                    Спасибо за внимательное прочтение!
                    Исправил.
                      0
                      тот, что по умолчанию — да.
                      но вообще если пергружать, возвращать он может и объект, но в случае объекта, будут проблемы с конструкциями вида a=b=c
                        0
                        И в чем же будет проблема в приведенном вами примере?
                          0
                          ну как же a=(b=c)

                          1. b=c создался новый объект, равный новому значению b
                          2. a=(b=c) этот объект ушел аргументом (пусть ссылкой) и создался внутри новый объект, который должен быть возвращен.

                          Т.е. 2 ненужных создания объекта (в таких случаях нужно просто возвращать ссылку на *this).
                            0
                            естественно, проблемы оптимизационные.
                            очень советую «Эффективное использование С++» Скотта Майерса.
                              0
                              Я под «проблемами» понял «неожиданное поведение программы», поэтому и спросил :) Лишние копирования — да, я просто не решился назвать их проблемами :)
                                0
                                ну я эм, подразумевал, что «так делать не надо», но резковато выразился, да.
                            0
                            В конструкции a=b=c серьезной проблемы не будет, а вот при желании написать (a=b).foo() функция будет применена к временному объекту, состояние 'a' не изменится. При этом все отлично скомпилируется, а где ошибка придется долго чесать репу.
                              0
                              Ага, всё верно :) А спрашивал я потому, что именно в приведённом примере проблемы как таковой не было. Многие знают, что принято возвращать ссылку, но не знают чем именно чревато возвращение экземпляра. Некоторые ещё пытаются возвращать const Type&, но в этом случае хоть компилятор ругнётся, если foo() не const, а вот если возвращать Type — нет.
                        0
                        Еще можно добавить описание проблемы дефолтного невиртуального деструктора.
                          0
                          Это у меня в планах… хотя следующий топик я планирую не про это. Так что тема про виртуальные деструкторы не занята! :-)
                            0
                            Занята ;)
                            –1
                            Наследование реализации — это плохо, очень плохо :-)
                              0
                              Но иногда — нужно, очень нужно :)
                            –3
                            ИМХО, там где вы изобразили «вот такой класс»… вы допустили достаточно весомую ошибку… вы забыли указать тела всех указаных вами функций..(конструткоры, деструкторы — все они функции) хотя бы в виде пустых скобок {}. Если в голову прийдется мысль «имелось в виду что они в соовтествующем *.срр» это тоже будет заблуждением… компилятор генерирует тела по умолчанию именно в месте обьявления класса. В этом легко убедиться кстати, но думаю раз вы пишите статьи по языку то мои примеры окажутся излишними
                              +1
                              Смело приводите излишние примеры, потому, что я вас не понял совсем.
                              Это прототипы. Они без тел. В конце заметки я как раз пишу, что оставить всё именно так — одно из решений («Самый простой способ»).
                              компилятор генерирует тела по умолчанию именно в месте объявления класса
                              Вот тут не соглашусь (или я вас не понял?). Он их генерит, если они нужны. То есть если вы нигде не использовали operator=, то он сгенерирован не будет.
                              Вот такая программа скомпилится и будет вполне работоспособна
                              class A {
                              public:
                                A();
                              };
                              int main() {
                                // A a;
                                return 0;
                              }
                              
                              А если убрать комментарий, то компилятор задумается «как же этот объект-то создать? где ж конструктор? ба! нет конструктора! undefined reference to `A::A()'»
                              (у меня gcc 4.2.1)
                                –4
                                Я не гвоорил что он их генерит обязательно… потому ваше несогласие здесь вообще не как не клеится. Я говорил про место где тела потенциально будут прописаны, т.е. если мы доверяем компилятору сгенерировать нам конструктор по умолчанию, мы получаем A() {}, inplace тело, а не сигнатуру в .h и тело в .cpp, или вы не ощущаете разницу которую эт повлечет?

                                  +1
                                  Неа, не ощущаю. Что значет «про место где тела потенциально будут прописаны»? что за потенциальное описание? С этого места уже не ясно о чём речь. Предложение «если мы доверяем компилятору сгенерировать нам конструктор по умолчанию, мы получаем A() {}, inplace тело, а не сигнатуру в .h» вообще не понял. Компилятор никогда не генерит никаких «сигнатур» (что это?) в .h и тел в .cpp.

                                  Ничего не понял. Вы примеры обещали. Выкатывайте! (я действительно хочу понять; вы же меня понимаете,-) я тоже хочу!)
                                    –4
                                    «Потенциально», это то к чему вы апелировали в 1 ответе: потенциально подразумевает что может будет пристуствовать, может нет, зависимо от того есть ли обращения к этому символу (только не спрашивайте что такое символ и причем тут символ, а то я заплачу и уйду обсуждать с++ с кем то другим)
                                    Не знаете что такое сигнатура? жаль… или вы читали лишь 1 автора, или авторов которые списывали друг у друга… Сигнатура функции это тип её возвращаемой значения, имя (опционально) и набор типов параметров… сигнатура это же «прототип». До примеров мы еще дойдем, сначала надо быт увереным что вы в теме
                                      +1
                                      Что такое символ знаю, что такое сигнатура оказывается тоже знаю.
                                      (Вы мочему-то на мне как-то зациклились; тут же много людей читает; пишите примеры; я не пойму — другие поймут)
                                        –5
                                        Зациклился я на вас потому что вы пишете статью по языку, при этом явно не имея фундаментальных знаний(мне так показалось, извините если я не прав), тогда грош ей цена.
                                        Да я собственно пришел сюда не учить людей С++, Хабра не место для этого, я вообще не нашел пока тут не одной нормальной статьи по каким то языковым фичам, все перепачатки да вольнодуства на тему базовых знаний… оно то полезно для новичков но при чем тут Хабр… не знаю, увы. Любая книга принесла бы куда больше пользы (даже книги Шилтда, да, даже они))))

                                        Вот вам пример, так и быть:
                                        Some.h:
                                          0
                                          Как вы негативно всё воспринимаете! Предлагаю другую точку зрения: автор осёл (допустим) — сообщество толковое; статья плохая — обсуждение интересное…

                                          Я так понимаю, что Some.h ещё будет?
                                            –3
                                            оборвало коммент…
                                            Some.h:

                                            class B; //fwd decl

                                            class A
                                            {
                                            boost::shared_ptr bPtr_;
                                            }

                                            Some.cpp
                                            #include Some.h


                                            A a;


                                            При компиляции ругнется на incompete type B именно по той причине по которой я придрался к вашей статье… знаете почему так происходит?
                                              +2
                                              Вы статью внимательно прочитали? Мораль была как раз в том, чтобы объявлять, но не определять, дабы потом получить ошибку при попытке использования.
                                                –4
                                                А вы вопрос внимательно прочитали? я предлагаю не определять, пусть компилятор определит… и всё равно будет ошибка… вы вопрос нормально поняли?
                                                  0
                                                  Нет, честно говоря, не понял. Я вас понял так «компилятор генерирует конструкторы и операторы присваивания инлайновыми». И что с того?
                                                    –3
                                                    с того — много чего, в приведенном мною примере при компиляции деструктора класса А, тип В будет назван неполным именно потому что деструктор А заинлайнен в заголовочном файле, для решения проблемы достаточно просто обьявить дестсруктор и обьявить пустое тело в.срр файле.
                                                    С одной стороны — что мы что компилятор обьявили абсолютно пустой деструктор, у вас создается впечатление что мы проделали дурную работу.
                                                    С другой — если руками все соберется, если автоматически — фигли…
                                                      +1
                                                      Тип B будет назван неполным потому, что компилятор в месте его использования ничего о нём не знает, кроме того, что он есть.
                                                        –2
                                                        что вы тут видите местом использования… какая именно строка кода. назовите.
                                                          0
                                                          A a;
                                                            –6
                                                            отнюдь, там где А а, можно после Some.h покдлючить B.h, но это дело не исправит
                                                              +2
                                                              Вы свои примеры компилировать пробовали, перед тем, как что-то утверждать?
                                                    0
                                                    Статья не про
                                                    class B;
                                                    а про
                                                    class B {};
                                                    (листинг в заголовке)
                                                      0
                                                      к чему это вы?
                                                        +4
                                                        Ну всё. Забейте. Я тоже не понимаю, к чему вы это. Не судьба.
                                          0
                                          Если мне память не изменяет, в С++ тип возвращаемого значения к сигнатуре функции как раз не относится.
                                            –1
                                            возвращаемой значение не участвует в определении кандидатов при перегрузке, но к сигнатуре относится
                                              0
                                              Вынужден с Вами не согласиться. Сигнатура функции — это то, по чем можно уникально идентифицировать одну единственную, конкретную функцию. В С++ для этого транслятору возвращаемый тип не нужен.
                                                +1
                                                Перед тем, как что-то утверждать, полезно не только компилировать примеры, но и читать стандарт:
                                                1.3.10 signature [defns.signature]
                                                the information about a function that participates in overload resolution (13.3): the types of its parameters and, if the function is a class member, the cv- qualifiers (if any) on the function itself and the class in which the member function is declared.2) The signature of a function template specialization includes the types of its template arguments (14.5.5.1).

                                                2) Function signatures do not include return type, because that does not participate in overload resolution.
                                                  0
                                                  Да я согласен, человеку свойственно забывать.
                                                  Кстати перед тем как писать статьи о языке стандарт надо не только прочитать, а еще и понять, этим тут не пахнет мне показалось
                                              0
                                              >Сигнатура функции это тип её возвращаемой значения, имя (опционально) и набор типов параметров… сигнатура это же «прототип». До примеров мы еще дойдем, сначала надо быт увереным что вы в теме

                                              Параметры (parameters) — объявления типов данных передаваемых функции.
                                              Аргументы (arguments) — реальные значение передаваемые при вызове.
                                              Сигнатура (signature) — имя функции и перечень параметров (без возвращаемого значения).
                                              Переменная (variable) — это место в памяти компьютера где можно хранить данные и извлекать их оттуда.
                                      –1
                                      чтобы не было неочевидностей и недодуманностей в ИДЕшках при создании классов дефолтные методы должны создаваться автоматически без возможности удаления — только переопределение
                                      Если же кодишь в каком-нибудь vim, то скорее всего достаточно умен, чтобы помнить тысячу тонкостей и иметь полную свободу
                                        0
                                        Угу, вам нужна одна кнопка «Сделать Хорошо». Что бы уж вообще ничего не напутать не дай бог.
                                          +2
                                          Если хочется все время полагаться на IDE — прямая дорога к C#, там многие тонкости урезаны на уровне языка. C++ язык требующий знания деталей реализации, различных тонкостей, но в ответ дает куда большую свободу. Лично мне в нем это и нравится.
                                            0
                                            в чем конкретно вы видите ограничение свободы, на примере статьи?
                                              0
                                              Попытка требовать обязательного определения методов — уже условность. Не важно, на уровне среды или же компилятора. Идеология языка подразумевает наличие множества стилей программирования. Кто-то любит условности и определяет все методы, другой держит в голове какие классы не могут копироваться.
                                                0
                                                Свобода совершать трудноисправляемые ошибки, чего только стоят холивары ++i vs i++, const==var vs var==const
                                          0
                                          Возможно, стоит упомянуть, что компилятор также добавляет в класс Empty фиктивный член, чтобы размер объекта класса не был нулевым.
                                            0
                                            Я не написал по трём причинам.
                                            1. Я не уверен, что оно создаётся. Вы уверены? Прям для всех компиляторов? Где почитать? (Спасибо)
                                            2. Эти данные мне кажутся какими-то странными (может их и нет совсем). На пример, они не складываются при наследовании. Пример:
                                            2.1. Нормальные данные складываются:
                                            class E {int x;};
                                            class EE : E {int x;};
                                            ...
                                            E e;
                                            cout << sizeof(e) << "\n";
                                            EE ee;
                                            cout << sizeof(ee) << "\n";
                                            
                                            Получаем:
                                            4
                                            8
                                            
                                            2.2. Но волшебные данные не складываются
                                            class E {};
                                            class EE : E {};
                                            ...
                                            
                                            Получаем:
                                            1
                                            1
                                            
                                            Это как-то странно. У вас есть объяснение?
                                            3. Я описал публичные методы. А данные, о которых вы говорите, являются приватными. Ну а если их не видно, то можно считать, что их нет :-)

                                            Спасибо, что застолбили тему про виртуальные деструкторы. Мне разные люди уже много раз говорили, что тема незаслуженно обделена вниманием.
                                              0
                                              Если класс абсолютно пустой, то откуда по-Вашему у него размер в 1 байт?

                                              1. Я тоже не уверен, что для всех компиляторов. И вряд ли в стандарте об этом сказано, но, если я не ошибаюсь, там сказано, что объект пустого класса должен иметь ненулевой размер.

                                              2. А получается так потому, что этот фиктивный член нужен только для того, чтобы у объектов класса был ненулевой размер. Если у меня в иерархии будет 20 классов, то это нужно тратить целые 20 байт? А зачем, если хватит и одного? Более того, если в классе есть хотя бы одна виртуальная функция, то этот самый фиктивный член не будет создан, потому что с его функциями будет отлично справляться скрытый указатель на таблицу виртуальных функций. Я об этом упоминал когда-то в своей галиматье.

                                              3. Ну да, с практической точки зрения можно считать, что его нет =) Но в общеобразовательных целях можно и такую мелочь запомнить (:

                                          Only users with full accounts can post comments. Log in, please.