Б. Страуструп - Дизайн и Эволюция C++. 2006 (1160775), страница 22
Текст из файла (страница 22)
Любой объект данного класса содержит скрытый указатель (часто его называют чрс г) на таблицу виртуальных функций своего класса. Если имеются определения с1авв д гпс ат риЫгс: чгггаа1 чо1с) 1()т ч1ктва1 чо1«) д(гпс); ч1гопа1 чо1() ц(белые); с1авв в : рпЫгс А ( рпЫгс: 1пс Ы чо(г) П((пг); // замещает дг:д() ч1кваа1 чо16 щ(в*) г ); с1авв С : риЫгс В ( рпы1с: гпс ст чо16 )г(г)оиые)г // замещает А:гц(] ч1ктаа1 чоЫ п(С*); то объект класса С выглядит примерно так, как показано на рис. 3.2.
просто искалеченные указатели на функцию, а потому избыточны. Более того, нногла мне возражали, что хорошо спроектированной программе не нужны ни расширяемость, ни открытость, обеспечиваемые виртуальными функциями, а путем анализа удается установить, какие невиртуальные функции можно вызывать напрямую. Поэтому, говорили оппоненты, виртуальные функции — простое свидетельство плохо проделанной работы. Разумеется, я с этим не соглашался и лобавлял виртуальные функции повсюду.
Я намеренно не предоставил в С++ механизма лля явного опроса типа объекта; «Имеющееся в 5«пч)о67 предложение ))(5РЕСТ сознательно не включено в С++. Это сделано для того, чтобы заинтересовать программистов в использовании модульного дизайна зо счет применения виртуальных функ цийь [5)гочз)гчр, ) 986]. Предложение НЕРЕСТ в 5)шцга — это не что иное, как эыфсс)т по поддерживаемому системой полю типа, Однако я видел в этом решении много минусов и решил, насколько возможно, придерживаться статического контроля типов и виртуальных функций. В конце концов, механизм идентификации типа во время исполнения был-таки добавлен в С++ (см. раздел 14.2). Но я надеюсь, что он все же окажется менее привлекательным, чем предложение Пх)БРЕСТ.
ПЛИИИИВИВ Рождение С++ а гргг ь с Рнс. 3.2 Вызов виртуальной функции преобразуется компилятором в вызов по указателю. Например, чозд г(С* р) ( р->р(2); превращается в (*(р->чрьг(1)))(р,2)) /* сгенернрсванннй код */ Эта реализация не елинственно возможная. К ее достоинствам можно отнести простоту и эффективность, к недостаткам — необходимость перекомпиляции при кажлом изменении множества виртуальных функций класса.
Таким образом, объект перестает быть просто конгломератом данных-членов. В языке С++ объект класса с виртуальными функциями кардинально отличается от простой структуры в С. Так почему же было решено разделить понятия класса и структуры? Дело в том, что я хотел иметь единую концепцию: олин набор правил размещения в памяти, один набор правил поиска, простые правила разрешения имен и т.л. Единая концепция обеспечивает лучшую интеграцию языковых срелств и упрощает реализацию. Я был убежден, что если бы зсгнсс воспринималось пользователями как «С и совместимость», а с1азз — как «С++ и более развитые возможности», то все сообщество раскололось бы на два лагеря, общение между которыми вскоре прекратилось бы.
Для меня было очень важно, чтобы прн проектировании класса число используемых языковых средств сократилось до минимума. Лишь единая концепция соответствовала моей идее о плавном и постепенном перехоле от «традиционного стиля программирования на С» через понятие абстракции данных к объектно-ориентированному программированию. Оглялываясь навал, я думаю, что такой подход в немалой степени способствовал успеху С++ как инструмента для решения практических задач. На протяжении ряда лет выдвигались самые разные лорогостояшие илеи, которые могли быть реализованы «только для классов», а для структур предлагалось оставить низкие накладные расходы и облегченную функциональность.
Но, по-моему, приверженность единой концепции зсгисс и с1авз избавила нас от включения в классы дополнительных полей и т.п., радикально отличающихся от того, что мы сейчас имеем. Другими словами, принцип «зс кис с — это с1аз з» уберег С++ от превращения в гораздо более высокоуровневый язык с независимым полмножеством низкого уровня, хотя некоторые, возможно, и желали такого развития событий. Виртуальные функции МИИИИИИИ 3.5.2. Замещение и поиск подходящей виртуальной функции Виртуальная функция может быть замещена только в производном классе и только функцией с такими же типами аргументов, возвращаемого значения и именем.
Это позволило избежать проверки типов во время исполнения и не хранить в выполняемой программе обширную информацию о типах. Например: с1авв заве ( раЫгс: нггтаа1 чоьд Г(); ч1гтва1 чогд д(гпг); ); с1аяв реггчед : раЫгс заве ( риЫ1с: чоьд Г(); // замещает Вазе:мт() чоьд р(с11аг); Г/ не замещает Вазе::р() Злесь скрыта очевидная ловушка. Невиртуальная функция ()ег1чес):: д ( ) никак не связана с виртуальной функцией Паве:: д () и скрывает ее. Если вы работаете с компилятором, не выдающим предупреждений по зтому поводу, то здесь может возникнуть проблема. Однако обнаружить такую ситуацию компилятору не составляет труда.
С)гонг 1.0 не давал таких предупреждений, чем вызвал немало нареканий. Уже в версии 2.0 зто было исправлено. Правило точного соответствия типов лля замешаюших функций позже было ослаблено в отношении типа возврашаемого значения (см. раздел 13.7). 3.5.3. Сокрытие членов базового класса Имя в производном классе скрывает любой объект или функцию с тем же именем в базовом классе. Несколько лет продолжалась полемика на тему, разумно ли такое решение. Данное правило впервые введено в С ю(гп С!аваев. Я считал, что зто естественное следствие обычных правил областей действия. Отстаивая свою точку зрения, я говорил, что противоположная позиция — поместить имена из базового и производного классов в единую область действия — приводит к не менее многочисленным трудностям.
В частности, по ошибке можно вызвать из подобъектов функции, изменяюшие состояние: с1авв Х ( гоя х; риЫ(с: чгггиа1 но(д сору(Х* р) ( х = р->х; ) ): с1авв ХХ : ряшгс Х ( !ос хх; риЫгс: нггяяа1 чогд сору(хх* р) ( хх = р->хх; х::сору(р); ) ): Рождение С++ БИИИИИИВ чоьб 1(Х а, ХХ Ь) ( а.сору(аЬ); // правильно: скопировать часть Ь, принадлежааую Х Ь.сору(ьа); // оиибка: сору(Х*) скрыто за сору(хХ*) ) Если разрешить вторую операцию копирования — а так и случилось бы в случае объединения областей действия, — то состояние объекта Ь изменилось бы лишь частично. В болыпинстве реальных ситуаций это привело бы к очень странному поведению объектов класса хх.
Я вилел, как люди попадались в эту ловушку при использовании компилятора СЯ() С++ (см, раздел 7.1.4), разрешающего такую перегрузку. Если функция сору () виртуальна, то можно считать, что хх:: сору ( ) замещает х:: сору (), но тогда лля нахождения ошибки с Ь. сору (йа) потребовалась бы проверка тинов во время исполнения, и программистам пришлось бы страховаться от таких неприятностей (см. раздел 13.7.1). Это я понимал уже тогда и боялся, что есть и другие, не осознанные мной проблемы. Вот почему пришлось остановиться на таких правилах, которые казались мне самыми строгими и одновременно наиболее простыми и эффективными в реализации. Теперь уже я подозреваю, что с помощью правил перегрузки, введенных в версии 2Л) (см. разлел 11.2.2), можно было справиться с этой ситуацией.
Рассмотрим вызов Ь. сору ( а а ) . Переменная Ь по типу точно соответствует неявному аргументу хх:: сору, но требует стандартного преобразования лля соответствия х:: сору. С лругой стороны, переменная а точно соответствует явному аргументу х:: сору, но требует стандартного преобразования лля соответствия хх:: сору. Следовательно, при разрешенной перегрузке такой вызов был бы признан ошибочным изза неоднозначности.
О явном задании перегрузки функций в базовом и производном классах рассказывается в разделе 17.5.2. 3.6. Перегрузка Некоторые пользователи просили меня обеспечить возможность перегрузки операторов. Инея выглядела заманчиво, а по опыту работы с А!Во!68 я знал, как ее реализовать. Однако вволить понятие перегрузки в С++ мне не хотелось по следующим причинам: ы реализовать перегрузку нелегко, а компилятор при этом разрастается ло чуловнщных размеров; гз научиться правильно пользоваться перегрузкой трудно и столь же трулно корректно определить ее. Следовательно, руководства и учебники тоже разбухнут; з кол, в котором используется перегрузка, принципиально неэффективен; з перегрузка затрулняет понимание программы.
Если признать верными 3-й и 4-й пункты, то С++ лучше было бы обойтись без перегрузки. А если истинны были бы 1-й илн 2-й, то у меня не хватило бы ресурсов лля поддержки перегрузки. Перегрузка ИИИИИИИН Олнако если все зти четыре утверждения являлись бы ложными, то перегрузка помогла бы решить множество проблем, возникавших у пользователей С++, Некоторые хотели, чтобы в С++ имелись комплексные числа, матрицы и векторы в духе АР]., многим лля работы пригодились бы массивы с проверкой выхола за границы диапазона, многомерные массивы и строки. Мне были известны, по крайней мере, лва разных приложения, где было бы полезно перегрузить логические операции типа ) (или), а (и) и (исключающее или). Список можно было бы продолжать, н с ростом числа пользователей С++ он только расширялся бы.
На пункт 4 — перегрузка «затуманивает» код — я отвечал, что некоторые из моих лрузей, чье мнение я ценил и чей опыт программирования насчитывал десятки лет, утверждают, что их кол от перегрузки стал бы только понятнее. Что с того, если, пользуясь перегрузкой, вы написали невразумительную программу? В конце концов, зто можно сделать на любом языке. Важно лишь научиться использовать то или иное средство во благо, а не во вред. Затем я убелил себя, что перегрузка не обязательно должна быть неэффективной [8ггоизтгвр,1984]з], [АКМ, $12.1с]. Детали механизма перегрузки были в основном проработаны мной, а также Стью Фельдманом, Дугом Макилроем и Джонатаном Шопиро.
Итак, получив ответ на третий вопрос о якобы «принципиальной неэффективности кода, в котором используется перегрузка», нужно было ответить на первый и второй — о сложности компилятора и языка. Сначала я отметил, что пользоваться классами с перегруженными операторами, например сопр1ех и зсгспд, было очень ле)тго. Затем написал разлелы руковолства, дабы доказать, что дополнительная сложность вовсе не так серьезна, как кажется.
Этот раздел занял меньше полутора страниц (в 42-страничном руководстве). Наконец, за два часа была написана первая реализация, которая заняла в С(гонг всего 18 лишних строк. Таким образом, я достаточно убедительно продемонстрировал, что опасения относительно сложности определения и реализации перегрузки преувеличены. Впрочем, в главе 11 показано, что некоторые проблемы все же имеют место.
Естественно, любой вопрос я решал, сначала доказывая полезность того или иного нововведения, а затем реализуя его. Механизмы перегрузки летально рассмотрены в работе [81гоцзггцр, 1984Ь], а примеры нх использования в классах— в [Козе, 1984] и [Жор]го, 1985]. Полагаю, перегрузка операторов была лля С++ хорошим приобретением. Помимо очевидного использования перегруженных арифметических операторов (+, *, +=, *= и т,л.) в численных приложениях, для управления доступом часто перегружают операторы взятия индекса [ ], применения ( ) и присваивания =, а операторы «и» стали стандартными операторами ввода/вывода (см.