6 февраля 2009 в 04:01

C++ MythBusters. Миф о виртуальных функциях

C++*
Здравствуйте.

В прошлой статье я рассказывал, с какой не всем известной особенностью можно столкнуться при работе с подставляемыми функциями. Статья породила как несколько существенных замечаний, так и многостраничные споры (и даже холивары), начавшиеся с того, что inline-функции вообще лучше не использовать, и перешедшие в стандартную тему C vs. C++ vs. Java vs. C# vs. PHP vs. Haskell vs. …

Сегодня пришла очередь виртуальных функций. И, во-первых, я сразу оговорюсь, что статья моя (в принципе, как и предыдущая) ни в коей мере не претендует на полноту изложения. А во-вторых, как и раньше, эта статья не для профессионалов. Она будет полезна тем, кто уже нормально разбирается в основах C++, но имеет недостаточно опыта, либо же тем, кто не любит читать книжек.

Надеюсь, все знают, что такое виртуальные функции и как они используются, так как объяснять это уже не моя задача. Уверен, что RFry в цикле своих статей о C++ рано или поздно доберется и до них.

Если в материале про inline-методы миф был не совсем очевиден, то в этом — напротив. Собственно, перейдем к «мифу».

Виртуальные функции и ключевое слово virtual

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

  1. #include <cstdlib>
  2. #include <iostream>
  3.  
  4. using std::cout;
  5. using std::endl;
  6.  
  7. struct A
  8. {
  9.   virtual ~A() {}
  10.  
  11.   virtual void foo() const { cout << "A::foo()" << endl; }
  12.   virtual void bar() const { cout << "A::bar()" << endl; }
  13.   void baz() const { cout << "A::baz()" << endl; }
  14. };
  15.  
  16. struct B : public A
  17. {
  18.   virtual void foo() const { cout << "B::foo()" << endl; }
  19.   void bar() const { cout << "B::bar()" << endl; }
  20.   void baz() const { cout << "B::baz()" << endl; }
  21. };
  22.  
  23. struct C : public B
  24. {
  25.   virtual void foo() const { cout << "C::foo()" << endl; }
  26.   virtual void bar() const { cout << "C::bar()" << endl; }
  27.   void baz() const { cout << "C::baz()" << endl; }
  28. };
  29.  
  30. int main()
  31. {
  32.   cout << "pA is B:" << endl;
  33.   A * pA = new B;
  34.   pA->foo();
  35.   pA->bar();
  36.   pA->baz();
  37.   delete pA;
  38.  
  39.   cout << "\npA is C:" << endl;
  40.   pA = new C;
  41.   pA->foo(); pA->bar(); pA->baz();
  42.   delete pA;
  43.  
  44.   return EXIT_SUCCESS;
  45. }
* This source code was highlighted with Source Code Highlighter.


Итак, имеем простую иерархию классов. В каждом классе определены 3 метода: foo(), bar() и baz(). Рассмотрим неверную логику людей, которые находятся под действием мифа:
когда указатель pA указывает на объект типа B имеем вывод:
pA is B:
B::foo() // потому что в родительском классе A метод foo() помечен как virtual
B::bar() // потому что в родительском классе A метод bar() помечен как virtual
A::baz() // потому что в родительском классе A метод baz() не помечен как virtual

когда указатель pA указывает на объект типа С имеем вывод:
pA is C:
С::foo() // потому что в родительском классе B метод foo() помечен как virtual
B::bar() // потому что в родительском классе B метод bar() не помечен как virtual,
// но он помечен как virtual в классе A, указатель на который мы используем
A::baz() // потому что в классе A метод baz() не помечен как virtual


С невиртуальной функцией baz() всё и так ясно. А вот с логикой вызова виртуальных функций есть неувязочка. Думаю, не стоит говорить, что на самом деле вывод будет следующим:

pA is B:
B::foo()
B::bar()
A::baz()

pA is C:
C::foo()
C::bar()
A::baz()


Вывод: виртуальная функция становится виртуальной до конца иерархии, а ключевое слово virtual является «ключевым» только в первый раз, а в последующие разы оно несет в себе чисто информативную функцию для удобства программистов.

Чтобы понять, почему так происходит, нужно разобраться, как именно работает механизм виртуальных функций.

Раннее и позднее связывание. Таблица виртуальных функций

Связывание — это сопоставление вызова функции с вызовом. В C++ все функции по умолчанию имеют раннее связывание, то есть компилятор и компоновщик решают, какая именно функция должна быть вызвана, до запуска программы. Виртуальные функции имеют позднее связывание, то есть при вызове функции нужное тело выбирается на этапе выполнения программы.

Встретив ключевое слово virtual, компилятор помечает, что для этого метода должно использоваться позднее связывание: для начала он создает для класса таблицу виртуальных функций, а в класс добавляет новый скрытый для программиста член — указатель на эту таблицу. (На самом деле, насколько я знаю, стандарт языка не предписывает, как именно должен быть реализован механизм виртуальных функций, но реализация на основе виртуальной таблицы стала стандартом де факто.). Рассмотрим этот пруфкод:

  1. #include <cstdlib>
  2. #include <iostream>
  3.  
  4. struct Empty {};
  5.  
  6. struct EmptyVirt { virtual ~EmptyVirt(){} };
  7.  
  8. struct NotEmpty { int m_i; };
  9.  
  10. struct NotEmptyVirt
  11. {
  12.   virtual ~NotEmptyVirt() {}
  13.   int m_i;
  14. };
  15.  
  16. struct NotEmptyNonVirt
  17. {
  18.   void foo() const {}
  19.   int m_i;
  20. };
  21.  
  22. int main()
  23. {
  24.   std::cout << sizeof(Empty) << std::endl;
  25.   std::cout << sizeof(EmptyVirt) << std::endl;
  26.   std::cout << sizeof(NotEmpty) << std::endl;
  27.   std::cout << sizeof(NotEmptyVirt) << std::endl;
  28.   std::cout << sizeof(NotEmptyNonVirt) << std::endl;
  29.  
  30.   return EXIT_SUCCESS;
  31. }
* This source code was highlighted with Source Code Highlighter.


Вывод может отличаться в зависимости от платформы, но в моем случае (Win32, msvc2008) он был следующим:

1
4
4
8
4


Что можно понять из этого примера. Во-первых, размер «пустого» класса всегда больше нуля, потому что компилятор специально вставляет в него фиктивный член. Как пишет Эккель, «представьте процесс индексирования в массиве объектов нулевого размера, и все станет ясно» ;) Во-вторых, мы видим, что размер «непустого» класса NotEmptyVirt при добавлении в него виртуальной функции увеличился на стандартный размер указателя на void; а в «пустом» классе EmptyVirt фиктивный член, который компилятор ранее добавлял для приведения класса к ненулевому размеру, был заменен на указатель. В то же время добавление невиртуальной функции в класс на размер не влияет (спасибо nullbie за совет). Имя указателя на таблицу отличается в зависимости от компилятора. К примеру, компилятор Visual Studio 2008 называет его __vfptr, а саму таблицу ‘vftable’ (кто не верит, может посмотреть в отладчике :) В литературе указатель на таблицу виртуальных функций принято называть VPTR, а саму таблицу VTABLE, поэтому я буду придерживаться таких же обозначений.

Что представляет собой таблица виртуальных функций и для чего она нужна? Таблица виртуальных функций хранит в себе адреса всех виртуальных методов класса (по сути, это массив указателей), а также всех виртуальных методов базовых классов этого класса.

Таблиц виртуальных функций у нас будет столько, сколько есть классов, содержащих виртуальные функции — по одной таблице на класс. Объекты каждого из классов содержат именно указатель на таблицу, а не саму таблицу! Вопросы на эту тему любят задавать преподаватели, а также те, кто проводит собеседования. (Примеры каверзных вопросов, на которых можно подловить новичков: «если класс содержит таблицу виртуальных функций, то размер объекта класса будет зависеть от количества виртуальных функций, содержащихся в нем, верно?»; «имеем массив указателей на базовый класс, каждый из которых указывает на объект одного из производных классов — сколько у нас будет таблиц виртуальных функций?» и т.д.).

Итак, для каждого класса у нас будет создана таблица виртуальных функций. Каждой виртуальной функции базового класса присваивается подряд идущий индекс (в порядке объявления функций), по которому в последствие и будет определяться адрес тела функции в таблице VTABLE. При наследовании базового класса, производный класс «получает» и таблицу адресов виртуальных функций базового класса. Если какой-либо виртуальный метод в производном классе переопределяется, то в таблице виртуальных функций этого класса адрес тела соответствующего метода просто будет заменен на новый. При добавлении в производный класс новых виртуальных методов VTABLE производного класса расширяется, а таблица базового класса естественно остается такой же, как и была. Поэтому через указатель на базовый класс нельзя виртуально вызвать методы производного класса, которых не было в базовом — ведь базовый класс о них ничего «не знает» (дальше мы все это посмотрим на примере).

Конструктор класса теперь должен делать дополнительную операцию: инициализировать указатель VPTR адресом соответствующей таблицы виртуальных функций. То есть, когда мы создаем объект производного класса, сначала вызывается конструктор базового класса, инициализирующий VPTR адресом «своей» таблицы виртуальных функций, затем вызывается конструктор производного класса, который перезаписывает это значение.

При вызове функции через адрес базового класса (читайте — через указатель на базовый класс) компилятор сначала должен по указателю VPTR обратиться к таблице виртуальных функций класса, а из неё получить адрес тела вызываемой функции, и только после этого делать call.

Из всего вышесказанного можно сделать вывод, что механизм позднего связывания требует дополнительных затрат процессорного времени (инициализация VPTR конструктором, получение адреса функции при вызове) по сравнению с ранним.

Думаю, на примере все станет понятнее. Рассмотрим следующую иерархию:



В данном случае получим две таблицы виртуальных функций:
Base
0
Base::foo()
1 Base::bar()
2 Base::baz()

и
Inherited
0
Base::foo()
1 Inherited::bar()
2 Base::baz()
3 Inherited::qux()


Как видим, в таблице производного класса адрес второго метода был заменен на соответствующий переопределенный. Пруфкод:

  1. #include <cstdlib>
  2. #include <iostream>
  3.  
  4. using std::cout;
  5. using std::endl;
  6.  
  7. struct Base
  8. {
  9.   Base() { cout << "Base::Base()" << endl; }
  10.   virtual ~Base() { cout << "Base::~Base()" << endl; }
  11.  
  12.   virtual void foo() { cout << "Base::foo()" << endl; }
  13.   virtual void bar() { cout << "Base::bar()" << endl; }
  14.   virtual void baz() { cout << "Base::baz()" << endl; }
  15. };
  16.  
  17. struct Inherited : public Base
  18. {
  19.   Inherited() { cout << "Inherited::Inherited()" << endl; }
  20.   virtual ~Inherited() { cout << "Inherited::~Inherited()" << endl; }
  21.  
  22.   virtual void bar() { cout << "Inherited::bar()" << endl; }
  23.   virtual void qux() { cout << "Inherited::qux()" << endl; }
  24. };
  25.  
  26. int main()
  27. {
  28.   Base * pBase = new Inherited;
  29.   pBase->foo();
  30.   pBase->bar();
  31.   pBase->baz();
  32.   //pBase->qux();    // Ошибка
  33.   delete pBase;
  34.  
  35.   return EXIT_SUCCESS;
  36. }
* This source code was highlighted with Source Code Highlighter.


Что происходит при запуске программы? Вначале объявляем указатель на объект типа Base, которому присваиваем адрес вновь созданного объекта типа Inherited. При этом вызывается конструктор Base, инициализирует VPTR адресом VTABLE класса Base, а затем конструктор Inherited, который перезаписывает значение VPTR адресом VTABLE класса Inherited. При вызове pBase->foo(), pBase->bar() и pBase->baz() компилятор через указатель VPTR достает фактический адрес тела функции из таблицы виртуальных функций. Как это происходит? Вне зависимости от конкретного типа объекта компилятор знает, что адрес функции foo() находится на первом месте, bar() — на втором, и т.д. (как я и говорил, в порядке объявления функций). Таким образом, для вызова, к примеру, функции baz() он получает адрес функции в виде VPTR+2 — смещение от начала таблицы виртуальных функций, сохраняет этот адрес и подставляет в команду call. По этой же причине, вызов pBase->qux() приводит к ошибке: несмотря на то, что фактический тип объекта Inherited, когда мы присваиваем его адрес указателю на Base, происходит восходящее приведение типа, а в таблице VTABLE класса Base никакого четвертого метода нет, поэтому VPTR+3 указывало бы на «чужую» память (к счастью, такой код даже не компилируется).

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

Также становится понятно, почему виртуальные функции работают только при обращении по адресу объекта (через указатели либо через ссылки). Как я уже сказал, в этой строке
Base * pBase = new Inherited;
происходит повышающее приведение типа: Inherited* приводится к Base*, но в любом случае указатель всего лишь хранит адрес «начала» объекта в памяти. Если же повышающее приведение производить непосредственно для объекта, то он фактически «обрезается» до размера объекта базового класса. Поэтому логично, что для вызова функций «через объект» используется раннее связывание — компилятор и так «знает» фактический тип объекта.

Собственно, это всё. Жду комментариев. Спасибо за внимание.

P.S. Данная статья помечена грифом «Гарантия Скора» ©
(Skor, если ты это читаешь, это для тебя ;)

P.P.S. Да, забыл сказать… Джависты сейчас начнут кричать, что в Java по умолчанию все функции виртуальные.
_________
Текст подготовлен в ХабраРедакторе

Progg it
Сергей Оленда́ренко @GooRoo
карма
105,2
рейтинг 0,0
Похожие публикации
Самое читаемое Разработка

Комментарии (109)

  • НЛО прилетело и опубликовало эту надпись здесь
    • +1
      DECLARE_INTERFACE, STDMETHOD, THIS_… :)
      кстати надо написать заметку о том, как связывать код на С с С++ через «ручные» виртуальные таблицы.
      • +2
        Напишите. Я почитаю с удовольствием.
  • +5
    Хорошая статья, большая работа, спасибо.

    Однако, оба мифа мифами не являются, а хорошо известны любому опытному C++ программисту, то есть в статье что-то новое могут открыть для себя программеры «случайно» пишущие на cpp или новички.

    Может быть просто дать две ссылки на документацию по cpp и не тратить время на подготовку кода, написание статьи?
    • –1
      Я рад, что Вы (как любой опытный С++-программист) всё это знаете о механизме работы виртуальных функций. Возможно, Вы с пеленок пишите компиляторы, поддерживающие позднее связывание и т.д. Но я бы советовал Вам научиться читать не только C++-код или документацию по C++ на английском языке, а и обычный русский текст:
      «…эта статья не для профессионалов. Она будет полезна тем, кто уже нормально разбирается в основах C++, но имеет недостаточно опыта, либо же тем, кто не любит читать книжек».
      • –2
        Спасибо за совет, буду стараться.

        Вам хочу посоветовать разобраться в терминах. Я говорил об опытных программистах, а не о профессионалах.

        Тем более что виртуальные функции — это основа и не владеть ими не может не то чтобы профессионал, а любой кто хоть раз их пробовал использовать.

        Вообще популяризация сложных механизмов всегда чревата :) Позвольте еще один совет:

        Популяризуйте язык не путем описания его базовых возможностей, а путем решения обычных или классических задач с привлечением нетривиальных возможностей языка.
        • 0
          > «Вам хочу посоветовать разобраться в терминах. Я говорил об опытных программистах, а не о профессионалах».

          Вы решили-таки не следовать моему совету: «…тем, кто уже нормально разбирается в основах C++, но имеет недостаточно опыта…»

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

          В мои задачи это не входит. Я не популяризирую язык, а лишь пытаюсь помогать (основываясь на личном опыте) тем, кто решил его выучить.
        • –2
          Да, забыл сказать. Можно постоянно пользоваться виртуальными функциями и не иметь ни малейшего представления, как они работают.
          • +2
            Можно и отверкой гвозди забивать, если у Вас такое в практике постоянно, то сочуствую Вашим коллегам, прошлым у будущим работодателям.
            • +8
              Хозяйке на заметку: шуруп, забитый молотком, держится крепче, чем гвоздь, закрученный отверткой ;)
              • НЛО прилетело и опубликовало эту надпись здесь
                • 0
                  Будьте проще. Вы сказали то, что и так очевидно, и не увидели в моих строках подтекста.
                  • НЛО прилетело и опубликовало эту надпись здесь
                    • –1
                      Прошу меня простить, но я не собираюсь Вам ничего разжевывать. Закроем дискуссию.
                      • НЛО прилетело и опубликовало эту надпись здесь
                        • 0
                          Во-первых, я ни перед кем не оправдываюсь и не собираюсь, потому что не вижу причин это делать.

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

                          В-третьих, я сам-то отлично понимаю смысл фразы «будьте проще», но, как уже сказал выше, Вам разжевывать это я не должен и не буду.
                          • НЛО прилетело и опубликовало эту надпись здесь
      • +2
        Вообще говоря, если меня что-то подобное спрашивают насчёт С++, я сначала задаю вопрос: а ты <C++ FAQ Lite читал?

        Статьи наподобие этой («миф о виртуальных функциях»), безусловно, полезны. Но если написать их штук триста, то мы получим C++ FAQ Lite!

        А у него есть несомненное преимущество: он уже написан.
        • 0
          У него есть неоспоримый недостаток: он на английском. И если на Хабре английский язык многие знают, то «посторонние» читатели (а таких немало) — далеко не всегда.
          • 0
            Недостаток надуманный. Для «посторонних» есть соответствующая «постороняя» ссылка
            • 0
              Честно, вот почитал я оба FAQ, как на английском, так и на русском, и что-то они меня не впечатлили. Мало написано.
              • 0
                Ну это и есть «lite». Впрочем, если распечатать его весь, получится вполне приличного объёма книжка. Т.е. если «с нуля» писать свой FAQ, можно рассчитывать, что достигнете их объёма в лучшем случае через год, а если считать по количеству затронутых тем — наверно, через 2-3 года.
                • 0
                  За ссылки, конечно, спасибо.
                  Ну а полный FAQ писать в мои планы не входило. Я не собираюсь охватывать все аспекты языка, но в некоторые мне особенно хотелось бы углубиться. Думаю, если бы я, как Вы говорите, написал подобных статей штук триста, то получилась бы не Lite-версия… :)
                  На этом, пожалуй, и закончим спор, если, конечно, Вы не против.
    • 0
      Субъективно, после семестра общения со студентами, могу сказать, что людям проще понять внятное объяснение «на пальцах», а не формальную техническую документацию.

      Впрочем мифами называть это действительно как-то слишком :)
      • 0
        С одной стороны многие люди очень долго пребывают в неведении касательно этих моментов, поэтому я назвал это «мифами».
        С другой стороны, это попытка придумать эффектное название — каюсь :)
      • 0
        охотно верю, однако C++ это не тот инструмент, который можно понять «на пальцах».

        с одной стороны: студентам нужно объяснять где найти материал (это классика инженерного образования) и требовать от них желания самостоятельно разобраться в вопросах.

        с другой стороны: услышав некоторое поверхностное объяснение большинство на этом и остановится, а через год будет позиционировать себя C++ специалистами, тем самым дескредитировав Вас и Ваше учебное заведение.
        • 0
          Студентам второго курса вполне можно объяснить :)
          И объяснение через цепочку «понять зачем надо на бытовых аналогиях» -> «посмотреть как работает внутри» -> «понять, как это использовать» получается довольно эффективным. Судя по моим наблюдениям.
  • 0
    что такое таблица виртуальных функций как-то никогда вопросов не возникало, а как на ее основе реализовать множественное наследование — это вопрос.
    или тогда создается несколько таблиц, для каждого из корневого базового класса? в таком случае, при активном использовании интерфейсов количество таблиц будет расти в геометрической прогрессии?
    • 0
      и чего это мы кричать начнем? ну виртуальные все, и что? :)
      • 0
        Разные люди попадаются. Не хотел Вас обидеть :)
    • +2
      создается несколько таблиц и при приведении типа указатель на объект сдвигается на начало нужный таблицы. компилятор сам отслеживает эти сдвиги. подробнее можно поискать по слову thunking, в википедии, кажется, есть ссылка на статью Страуструпа по этому поводу.
      • 0
        Верно. Правда, еще есть виртуальное наследование, но это отдельная тема.
      • 0
        не на начало таблицы, а на позицию ее в объекте (т.е. чтобу нужный vptr стал первым), конечно
  • НЛО прилетело и опубликовало эту надпись здесь
    • +1
      А компилятор присоединяется к мозгу программиста что ли? Откуда он знает что я хотел ввиду перекрыть или объявить новую? И почему будет ошибка во время выполнения то? Линковщик обругается скорее всего.
      • НЛО прилетело и опубликовало эту надпись здесь
        • –6
          Не понимаю я профита, заставляют ещё одно ненужное слово писать. А грамотрая IDE должна сама подсказать.
          • –2
            Полностью согласен. Компилятор не должен отлавливать опечатки.
            • НЛО прилетело и опубликовало эту надпись здесь
              • 0
                Вообще-то их должны отлавливать программисты во время отладки/тестирования.

                А то мы так можем начать требовать, чтобы компилятор отлавливал null-указатели. Руководствуясь той же логикой: «ну не пользователям же их отлавливать, верно?»
                • 0
                  А в чём проблема с отлавливанием null-указателей компилятором? Function Attributes
                  • 0
                    Скомпилируйте, пожалуйста, приведенные в этом тексте примеры при помощи компилятора MSVC
                    • –1
                      Ну зачем говорить о недостатках MSVC ;)

                      Я отвечал на «А то мы так можем начать требовать, чтобы компилятор отлавливал null-указатели»
                      gcc отлично с этим справляется, если у MSVC с этим проблемы — это уж вы сами выбрали такой инструмент ;)
                      • –1
                        Покажите мне пожалуйста, где эти атрибуты описаны в стандарте C++, прежде чем говорить о недостатках инструментов, которые я выбираю.
                        • 0
                          А я и не говорил о стандартах языка, речь шла о компиляторах «А то мы так можем начать требовать, чтобы компилятор отлавливал null-указатели»
                          Но я конечно не знаю всех возможностей MSVC(не лежит к нему душа), но можете попробовать доплатить 5000$ за Visual Studio Team System и воспользоваться статичным анализатором от Майкрософта «cl.exe /analyze» ;)
                          • 0
                            Я с таким успехом могу в компилятор натолкать таких фич, которые превратят C++ в другой язык (это я утрированно, но всё же).
                            Мне нужен переносимый код!
                            • 0
                              >Я с таким успехом могу в компилятор натолкать таких фич, которые превратят C++ в другой язык (это я утрированно, но всё же).
                              Натолкайте и продавайте этот продукт :) неплохо заработаете, что собственно многие и делают. Но видимо нынешние крутые программисты не слышали слов «Static analyze».

                              >Мне нужен переносимый код!
                              #ifdef GСС
                              #define nonnull __attribute__…
                              #else
                              #define nonnull
                              #endif
                              • 0
                                Пфф :) Давайте еще подо все компайлеры ставить дефайны — вдруг в других придумали какие-то свои атрибуты или еще чего… Это IMHO не самый лучший вариант.
                                • 0
                                  >Давайте еще подо все компайлеры ставить дефайны
                                  Так и скажите, что не писали крупных переносимых проектов без использования boost/qt…
                                  • 0
                                    Не скажу.
                                    Хотя в некоторых частях boost был-таки…
                                    • 0
                                      В любом случае, то что Вы предлагаете — не панацея.
                                      • 0
                                        >В любом случае, то что Вы предлагаете — не панацея.
                                        В любом случае, вы сами выбираете инструменты, которые делают вашу жизнь проще… я лишь хотел донести, что компиляторы отлавливают такие примитивные вещи как «null-указатели»…
                                        бегло посмотрев сейчас на майкрософтовский анализатор, который идёт в комплекте VS TS, увидел что и он с лёгкостью справляется с этой задачей и у него свои ключевые слова для определения семантики.
                • +1
                  Кстати, в новый стандарт C++ добавили литерал nullptr…
                • НЛО прилетело и опубликовало эту надпись здесь
                  • +1
                    Для этого давно придумали автоматическое тестирование. Уж что-что, а описки в именах они отлавливают просто на раз. Простейший тест для случая, приведённого ниже, будет выглядеть так:

                    def classAtestDoubleIt() :
                        a = A()
                        oldValue = a.something
                        a.doubleIt()
                        if a.something == oldValue * 2 :
                            return True
                        else :
                            return False
                    


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

                    Или написание тестов — это тоже потеря производительности с риском не уложиться в бюджет? ;)
                    • НЛО прилетело и опубликовало эту надпись здесь
                      • +1
                        Гораздо более трудоёмко — это ловить в разросшемся коде баги, которые могли быть устранены на самой ранней стадии. Лучше долго запрягать — но потом зато быстро ехать ;)
                        • НЛО прилетело и опубликовало эту надпись здесь
                          • +1
                            Синтаксис и так проверяется на самой ранней стадии. Код с неверным синтаксисом просто не скомпилируется (в случае C/C++).
                            Синтаксис языка однозначно определяется набором правил, и рассматриваемый нами пример синтаксически корректен. То, чего Вы хотите — это уже не проверка синтаксиса, для этого компилятор нужно наделить «более другими» мозгами.
                            Но лично я не очень хочу, чтобы излишне умный компилятор задалбывал меня вопросами «а ты уверен, что хотел написать именно setTxt, а не setText?»
                            А «ложные срабатывания» будут по-любому. AI всё ещё не создали. А если создадут, да такой, которого можно будет обучить программированию — то белковые программисты станут не нужны :)
                            • НЛО прилетело и опубликовало эту надпись здесь
    • +2
      с таким же успехом можно по ошибке перекрыть другую функцию
      • +2
        Успех совершенно не тот же и предназначено это не для борьбы с опечатками. Предназначено это для отлавливания проблем с рефекторингом. Если в библиотеке, которую вы используете поменяется сигнатура или название функции — в Java/Delphi/C# вы узнаете об этом на этапе компиляции, в C++ — только запустив программу. А если перекрываемая функция используется редко — то у заказчика на демонстрации.

        P.S. Конечно есть механизм, который часто спасает (абстрактные классы), но он слишком груб: нельзя дать возможность человеку перекрыть часть методов — либо всё, либо ничего.
    • 0
      А давайте возьмём Питон. Если напишем что-то вроде

      class A:
      def __init__ (self):
      self.something = 1

      def doubleIt(self):
      self.somethiing = self.something * 2

      a = A()
      print a.something
      a.doubleIt()
      print a.something

      то получим совсем не то, что хотели. И ошибку тоже не получим.
      Какой ужасный язык Питон! ;)
      • +1
        Чёрт, Хабр съел пробелы, и Питон моментально потерял своё лицо :)
        Ещё раз код:

        class A:
            def __init__ (self):
                self.something = 1
        
            def doubleIt(self):
                self.somethiing = self.something * 2
        
        a = A()
        print a.something
        a.doubleIt()
        print a.something
        • 0
          Вообще отловом подобных ошибок должен заниматься Lint (статический анализатор кода).
          Однако я не уверен, что подобный анализатор для С++ создавать рентабельно — другое дело языки описания аппаратуры типа Verilog и VHDL.
      • НЛО прилетело и опубликовало эту надпись здесь
        • НЛО прилетело и опубликовало эту надпись здесь
  • 0
    когда указатель pA указывает на объект типа B имеем вывод:

    pA is C:

    s/на объект типа B/на объект типа C/g
    • 0
      Исправил.
  • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      Ага, сам по неопытности попадался на этом ;)
    • 0
      По задумке, это моя следующая статья.
      • 0
        Про вызов виртуальных функций из конструктора базового класса тоже не забудьте ;)
        • 0
          Блин, ну Вы как в воду смотрите!… :)
  • +1
    Спасибо! Люблю статьи где рассказывается как это всё работает.
  • +1
    Для полноты примера стоило показать, что наличие невиртуальной функции не влияет на размер класса :)

    Также было бы интересно написать про множественные vtable при множественном наследовании и последующий thunking.
    • 0
      По поводу невиртуальной функции добавил. Спасибо.

      О множественном наследовании (в том числе виртуальном) может когда-то еще напишу.
  • 0
    Отличная статья, спасибо!
    P.S. У вас опечатка в картинке с классами Base и Inherited. viod вместо void
    • 0
      Спасибо, исправил.
  • 0
    «когда указатель pA указывает на объект типа B имеем вывод:» — строчка повторяется два раза, во втором случае, наверное, имелся ввиду «объект типа C»)
    • 0
      "… а в последующие разы оно несет в сеБе ..."
      • 0
        Стараюсь не допускать в статьях подобных погрешностей, но копи-паст — страшная штука :) К тому же набираю быстро, не всегда замечаю ошибки.
        • 0
          ворд в помощь) Иногда помогает отловить очевидные опечатки, но не панацея. Есть вроде ещё какие-то онлайн-сервисы проверки орфографии
          *на Liveinternet.ru, к примеру, можно проверить орфографию перед отправкой поста на сервер — пора бы и Хабру задуматься о такой фиче)*
          • 0
            Не люблю системы автоматической проверки орфографии. И кстати набирал в Word, но половину моих предложений он подчеркивает сплошной зеленой волнистой линией как «слишком сложное» или, иногда, «несогласованное».
  • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      Виртуальное наследование — это вообще сферический конь в вакууме. Все о нем знают, но никто его не пользуют, и правильно делают :)

      Страуструп предложил концепцию, реализация же лягла на плечи писателей компиляторов.

      > С другой стороны, именно поэтому в этих языках такая путаница — чем же отличаются интерфейсы от абстрактных классов. :)

      А тут все просто, в С++ нет интерфейсов :-P
      • 0
        Вообще многие пишут, что композиция предпочтительнее наследования. А множественное наследование и подавно можно «обойти стороной»…
        • +1
          И я с этим согласен. Апологеты ООП вообще часто пишут, что наследование само по себе вредно, так как нарушает инкапсуляцию объекта (хотя к плюсам с их множеством модификаторов доступа это отношение имеет в меньшей мере). И как бы, это отчасти верно. Одно дело, когда наследование обусловлено какими родственными отношениями между классами (т.е. объекты тесно связаны логически). Другое дело, когда используется наследование, только потому, что это «очень удобно», к примеру как в паттерне Bridge ГОФа.

          Т.е. я придерживаюсь точки зрения, что если задачу можно реализовать не используя полиморфизм, то лучше как раз его и не использовать, потому что полиморфизм и наследования — не всегда эффективно и расширяемо. А то получится, как «Любую задачу из мира ООП можно решить добавив новый уровень абстракции, кроме случая, когда уровней абстракции слишком много» :)
          • +1
            Тут какбе необходимо пояснить что вы имеете ввиду под полиморфизмом. Наследование интерфейсов это тоже полиморфизм, в нем, в отличие от наследования реализации, ничего такого ужасного нету.
        • 0
          И еще забыл добавить. Майерс к примеру в одной из свой статей (bishop3000 кажется на хабре пост писал), рекомендует все прайват методы объекта выносить в сторонние функции :)
        • 0
          Ты это только Александреску не говори, ок?:)
          • 0
            Вот когда начну на него фапать, как ты, тогда и посмотрим ;))
      • НЛО прилетело и опубликовало эту надпись здесь
      • 0
        >Все о нем знают, но никто его не пользуют, и правильно делают :)

        да неужели?:)
  • +3
    С точки зрения материала — статья гораздо лучше, чем предыдущая. Материал хорош, подан понятно. Одно но — стиль мне ваш не нравиться. Слишком уж пафосно. И действительно, согласен с товарищем выше — любой маломальски толковый плюсер все это знает и это далеко не миф.

    Ведь в становлении любого плюсера играют роль множество книжек талантливых мастеров. Начинающий, который прочтет вашу статью — мало что в ней поймет, скажет спасибо, а разбирательство с механизмом действия виртуальных функций оставит на потом, а прочев Майерса, а потом Саттера сам все постигнет. Именно по этой причине так мало статей по плюсам. Именно потому, что уже все сказано, а путь становления плюсера — однозначен.

    Я понимаю, что существует масса книг по С++, особенно популярны, почему-то, такие универсалы (у которых на каждый язык своя книга) как Шилдт, Экель, Дейтел, но это не совсем верный путь. Человек, который постигал плюсы по их книгам — упускает много важного по мере своего развития, и потом будучи уже разработчиком коммерческих приложений на плюсах сталкивается с сюрпризами.

    Путь любого плюсера:
    — Должен начинаться с чего-то простого и понятного. Например книжки Кернигана и Риччи по С. Или же C++ By Dissection by Ira Pohl — великолепная книга, где все простенько и со вкусом.
    — Можно продолжить пробежавшись по глазам по книге одного из универсалов (Шилдта к примеру). И очень часто консультироватся с талмудом Страуструпа.
    — А уж потом, Майерс, Саттер, Александреску, которые давным давно объяснили всю эту уличную магию и разложили по полочкам.
    — А еще профи плюсеры любят читать Dr. Dobbs Journal

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

    Таблица виртуальный функций не создается одна на класс. Она создается не per instance, а per type, т.е. если вы создали 5 экземпляров класса B, то таблица будет одна, и сидит она в статической памяти. Сделано все это, для экономии памяти :)
    • 0
      Я абсолютно согласен с тем, каким должен быть путь становления программиста С++. Но не каждый идет по «правильному пути». Можете считать, что именно для таких я и пишу.

      > «Таблица виртуальный функций не создается одна на класс. Она создается не per instance, а per type, т.е. если вы создали 5 экземпляров класса B, то таблица будет одна, и сидит она в статической памяти. Сделано все это, для экономии памяти :)»

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

      P.S. Вам ли говорить про пафос? :)
      • 0
        > А разве я не это же самое сказал? Я специально подчеркнул, что каждый экземпляр класса содержит лишь указатель на таблицу виртуальных функций.

        Сколько не перечитывал, не понял именно так, как нужно. Ваши слова не перечат сказаному мной, но из них никак нельзя напрямую вычленить истину новичку, посему предлагаю немного дополнить этот самый момент.

        PS А я пафосно стараюсь не писать :) Но вот мифологический стиль действительно как-то режет глаз.
        • 0
          > «Сколько не перечитывал, не понял именно так, как нужно. Ваши слова не перечат сказаному мной, но из них никак нельзя напрямую вычленить истину новичку, посему предлагаю немного дополнить этот самый момент».

          Я вроде бы нигде не говорил слов «экземпляр» или «объект». Я четко и ясно написал, что таблица одна на класс. Давайте Вы попробуете перефразировать так, чтобы новичку было понятно, а я, если что, исправлю.

          > «Но вот мифологический стиль действительно как-то режет глаз».

          Ну нужно же как-то заинтересовать новичков — сухих мануалов и документаций по С++ много. Мне хотелось сделать название хоть немного «живым».
    • +1
      Касательно последнего — вопрос терминологии. Для меня, к примеру, «класс» как раз и означает тип. От которого уже и создаются в памяти конкретные экземпляры(инстансы).
  • +1
  • 0
    Нужно было упомянуть о виртуальном деструкторе и его необходимости для всех виртуальных классов и их наследников. Это очень частая ошибка.
    • 0
      Ну не знаю. Согласен, ошибка довольно часто встречается у новичков. Но я четко выделил «миф» — очень распространенное заблуждение о том, что функция становится виртуальной только на один уровень иерархии…
  • 0
    VTABLE вообще интересная штука :) Люблю такие вот извращения:

    #include "stdafx.h"

    class CTest
    {
    private:
      virtual void Print()
      {
        _tprintf( _T("He he ;)\n"));
      }
    };

    class CX
    {
    public:
      virtual void BlaBla();
    };

    int _tmain(int argc, _TCHAR* argv[])
    {
      CTest t;

      //error C2248: 'CTest::Print' : cannot access private member declared in class 'test'
      //t.Print();

      //It works
      ((void (*)(void))((int*)(*(int*)&t))[0])();

      //It works, too
      ((CX*)&t)->BlaBla();

      return 0;
    }

    * This source code was highlighted with Source Code Highlighter.
    • 0
      Не дай-то Бог встретить такой код в чужом проекте.
    • 0
      Ужас какой…
      А если компиллер изменит порядок генерациии… ии… вообще страшно :D
      из цикла #define true false
      :-D

  • 0
    Спасибо за статью!

    С++ изучаю в институте. Рекомендованая литература — Б. Страуструп. По поводу виртуальных функций в его талмуде сказано очень не много, или (возможно, я что-то упустил), в нескольких местах. Так что связать позднее всё в единое представление о виртуальных функциях не получалось.

    Ключевым моментом для понимания стал механизм восходящего возведения типов.
    Спасибо!
    • 0
      Спасибо и Вам на добром слове.
  • 0
    Огромное спасибо!
    Давно не программил на плюсах и начинаю уже забывать важные нюансы. Было очень приятно почитать вашу отлично оформленную и грамотно написанную статью ( особенно спс за примеры — так очень быстро вникаешь ) и освежить свою память.

    Примного благодарю!
    • 0
      Спасибо, я старался.

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