Как стать автором
Обновить

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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
Вообще отловом подобных ошибок должен заниматься Lint (статический анализатор кода).
Однако я не уверен, что подобный анализатор для С++ создавать рентабельно — другое дело языки описания аппаратуры типа Verilog и VHDL.
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
когда указатель pA указывает на объект типа B имеем вывод:

pA is C:

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

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

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

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

Если интересно, попробуйте написать похожую статью про виртуальное наследование (ну и вообще, про множественное наследование). В частности — нужно ли ключевое слово virtual писать при определении каждого наследника, или достаточно только при определении самого первого. :)

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

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

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

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

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

Почему? При множественном наследовании сразу возникает эта проблематика.

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

Не совсем понял смысл тезиса (аргумента?). Это возражение или дополнение?
>Все о нем знают, но никто его не пользуют, и правильно делают :)

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

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

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

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

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

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

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

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

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

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

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

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

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

Ну нужно же как-то заинтересовать новичков — сухих мануалов и документаций по С++ много. Мне хотелось сделать название хоть немного «живым».
Касательно последнего — вопрос терминологии. Для меня, к примеру, «класс» как раз и означает тип. От которого уже и создаются в памяти конкретные экземпляры(инстансы).
Нужно было упомянуть о виртуальном деструкторе и его необходимости для всех виртуальных классов и их наследников. Это очень частая ошибка.
Ну не знаю. Согласен, ошибка довольно часто встречается у новичков. Но я четко выделил «миф» — очень распространенное заблуждение о том, что функция становится виртуальной только на один уровень иерархии…
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.
Не дай-то Бог встретить такой код в чужом проекте.
Ужас какой…
А если компиллер изменит порядок генерациии… ии… вообще страшно :D
из цикла #define true false
:-D

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

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

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

Примного благодарю!
Спасибо, я старался.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации