Б. Страуструп - Дизайн и Эволюция C++. 2006 (1160775), страница 72
Текст из файла (страница 72)
Я видел системы, время компиляции которых уменьшилось на порядок после того, как основные интерфейсы были реализованы в виде абстрактных классов. Попытка объяснить все это в 18ггопзггпр, 1986Ъ1 оказалась безуспешной, и лишь после появления в языке явной поддержки для абстрактных классов моя мысль стала понятной пользователям (2п(Ц. 13.2.3. Синтаксис Странный синтаксис =О был выбран вместо очевидной альтернативы ввести ключевое слово риге или аЬяг гасе только потому, что тогда я не видел возможности добавить новое ключевое слово.
Если бы я предложил риге, то версия 2.0 вышла бы без абстрактных классов. Чтобы не рисковать сроками выпуска версии и не навлекать на себя упреков по поводу нового ключевого слова, я воспользовался традиционным для С и С++ соглашением о том, что О означает «отсутствует». Синтаксис = О не противоречит моим взглядам натела функции как на инициализатор функции, а также представлениям (упрошенным, по обычно соответствуюшим реальности) о том, что множество виртуальных функций реализовано в виде вектора указателей на функции (см. раздел 3.5.1).
На самом деле, реализация синтаксиса =О путем помещения О в чгЬ1 — не лучший способ. Я помешал в чсь1 указатель на функцию роге ч(гсиа1 са11ес); затем эту фупкпню можно было определить так, чтобы она выводила подходящее сообщение об ошибке во время выполнения. Лучше объявлять отдельные функции исключительно виртуальными, а не весь класс — абстрактным, поскольку в этом случае удается добиться большей гибкости. Я высоко ценю возможность определять класс поэтапно, то есть часть виртуальных функций определить в базовом классе, а остальные — в производных. 13.2.4.
Виртуальные функции и конструкторы Конструирование объекта из базовых классов и членов (см. раздел 2.11.1) влияет на работу механизма виртуальных функций. Подчас по этому поводу возникала путаница. Поэтому здесь объясняется, почему избранный для С++ путь является почти безальтернативным. 1Э.2А.1 Вызов исключительно виртуальной функции Как вообще можно вызвать чисто виртуальную функцию, а не замещаю)дую ее в одном из производных классов? Объекты абстрактных классов могут существовать только в качестве базовых для каких-то других классов.
После того как обьект производного класса сконструирован, наша виртуальная функция уже замешена какой-то другой, определенной в производном классе. Однако в процессе конструирования собственный конструктор абстрактного класса может по ошибке вызвать исключительно виртуальную функцию: с1аяя А ( риь11с: ч1гсиа1 гяьг) Г() =О; МИИИИИИИ Абстрактные классы чо(а о(); лн; ); Незаконный вызов А:: б () легко обнаруживается компилятором. Однако А::д () может быть объявлена, скажем, как чоИ д::о() ( ((): ) в какой-то другой единице трансляции. Такую ошибку сможет обнаружить лишь компилятор, выполняющий перекрестный анализ нескольких единиц трансля- ции.
Альтернативой же является ошибка во время выполнения. 13.2.4.2. Конструирование в порядке асначала баэовыйл Я всегда предпочитаю проект, пе оставляющий места для ошибок во время выполнения. Но нет способа сделать програмьшрованне абсолютно безопасным. В частности, конструкторы создают среду, в которой работают функции-члены (см, раздел 2.11.1). Однако пока соаданпе такой среды не завершено, программисту даются частичные гарантии.
Рассмотрим пример: с1авв В ( рсЫ)с: 1лс Ь; чьгвеа1 чозб б(); чозб д(); Iг в(); с1авв О: риЫ1с В ( риЫьс: Х х~ чо1б 1(); !! з(); Компилятор может выдать предупреждения об обеих потенциальных проблемах. Если вы действительно собирались вызвать функцию 1' ( ) из самого класса В, напишите это явно — В:: й ( ) . д::д() ( Е() р о(); В::В() Ь+"; '() ' ) !! ощкбка: вызвана исключительно виртуальная функция (~ выглядит вполне безобидно /т неопределенное поведение: В::Ь еще не иннцнализнрована Ут вызывается: В:гб(), а не В;ьб() ИИИИИИИ1й Уточнения понятия класса Поведение данного конструктора контрастирует с тем, как пишется обыкновенная функция (зависящая от правильной работы конструктора): чоМ В::Я() Ь+к; // правильно, так как В::Ь вЂ” это член // В::В должен инициализировать его г(); // вызывается; Р::г(), если для Р вызывается В::д ) Разница между тем, какую функцию вызывает 1 ( ) при обращении из В:: В ( ) и из В:: ц ( ) в случае, когда В есть часть Р, может удивить неискушенного пользователя.
13.2.4.3. Что, если?.. Рассмотрим, что произошло бы при выборе альтернативного решения, когда каждый вызов виртуальной функции 1 ( ) приводит к вызову замещающей: чОЫ Р:гт() ( // операция, написанная из расчета на то, что Р::Х правильно // инициализировал в Р::Р ) Если бы замещающую функцию допускалось вызывать в процессе конструирования, ни в одной виртуальной функции не было бы гарантии, что объект будет правильно инициализирован конструкторами.
Поэтому в любой замещающей функции пришлось бы так же аккуратно контролировать все ошибки, как зто обычно делают в конструкторах. На самом деле написание замещающей функции оказалось бы сложнее, чем написание конструкторов, так как в последнем сравнительно просто определить, что было проинициализировано, а что нет. Автор замещающей функции, не имеющий гарантий того, что конструктор уже отработал, мог бы: о продолжать неоправданно надеяться на завершение инициализации; о пытаться защититься от неинициализированных базовых классов и членов.
Первый вариант лишает конструкторы привлекательности. Второй — практически нереализуем, поскольку у производного класса может быть много непосредственных и косвенных базовых классов, а во время выполнения нет возможности проверить, была ли инициализирована произвольная переменная. чзЫ Р::г() // нехарактерно для Саз ( ьг (Ьазе 1л1гта11гас)) ( // действие, опирающееся на то, что Р::Х // инициализирована Р:Р ) е1зе ( // сделать все возможное, не полагаясь на то, // что Р::Х инициализирована ) ЯИИИИИИИ Константные функции-члены Следовательно, если бы конструкторы вызывали замешаюшие функции, то для разумного написания последних пришлось бы значительно ограничить использование конструкторов.
При проектировании языка я принял такую точку зрения: пока конструктор объекта полностью не отработал, объект надо рассматривать, как недостроенное здание. В обмен на зто разрешается считать, что после завершения конструирования объект пригоден к использованию. 13.3. Константные функции-члены В С1гопс 1.0 константность поддерживалась не вполне последовательно, Когда реализация стала более строгой, в определении языка обнаружилось несколько прорех. Программисту нужно было предоставить возможность определять, какие функции-члены могут изменять состояние объекта, а какие — нет; с1авв Х (пп аа; рпб11с: чоЫ арбате() ( ааеа; ) (пс ча1ие() сопев ( теспгп аа; чоЫ спеас() сопев ( аа++; ) // ошибка: *сп1в константный ); Функция-член, объявленная с модификатором сопвб, например Х::ча1ие ( ), называется константной.
Гарантируется, что такая функция не изменяет значения объекта. Константную функцию-член можно применять как к константным, так и к не- константным объектам. Но неконстантная функция-член, такая как Х:: ирс1абе (), может применяться только к таким же объектам: 1пп д(Х о1, сопев Ха о2) о1.прдасе()/ // правильно о2.прдасе()/ // ошибка: о2 - константный объект геспхп о1.ча1пе() + о2.ча1пе(); // правильно ) 13.3.1.
Игнорирование сот1 при приведении типов С++ спроектирован для того, чтобы обнаруживать случайные ошибки, а не предотврашать намеренное емошенничествою Для меня зто означало, что должна существовать возможность принудительного игнорирования сопзс. Я думал, что Технически такая модель обеспечивается за счет того, что указатель спйв для неконстантной функции-члена класса Х указывает на Х, а для константной — на с оп в С Х. Различие между константными и неконстантными функциями позволяет выразить в С++ разделение между теми функциями, которые модифицируют состояние объекта, и теми, которые етого не делают.
Константные функции-члены оказались среди тех свойств языка, которые получили большую поддержку во время дискуссий на семинаре разработчиков компиляторов в Эстес Парк (см. раздел 7.1.2). ИИВИИ>ИВ Уточнения понятия класса компилятор не должен запрещать программисту явно обходить систему типов. Так, в (Ягопзггпр, 1992Ъ1 читаем: аИногда бывает попезно, чтобы объекты предстовпяпись попьзоватепям константными, но но самом депе меняли свое состояние. Такие кпоссы можно создавать с помощью явного приведения типов: с1авв ХХ ( (пс а; ьпс са11в ог (г 1пс т() сопев ( ((хх*)сЫв)->са11в ог 1+чг гесцгп а; // )г Если есть явное преобразование типа, значит, про~ромма спроектирована недостаточно удачно. Изменение состояния константна~о объекта может вводить в заблуждение, в определенном контексте приводить к ошибкам.
Если же объект находится в постоянной памяти, то этот прием и вовсе не работает. Лучше вынести переменную часть обьекта в отдельный объект: с1авв ХХХ тпс аг 1пга са11в ог гг гпс г() сопвс ( са11в ог 1++/ гесцгп а; ) // ххх() : са11в ог г(*пеы 1пс) ( /* ... */ -ХХХ() ( г)е1еге аса11в ог гг /* ... */ ) ): Не стоит забывать, что основная цель модификатора сопев — специфицировать интер. фейс, а не помочь оптимизатору. Следует также отметить, что свобода действий и гибкость иногда бывают полезны, но могут применяться и неправильно».
Включение оператора сопзс сазс (см. раздел 14.3А) позволяет программисту отличить приведения типов с целью отбрасывания с опас от тех, которые применяются для других манипуляций с типами. 13.3.2. Уточнение определения сот1 Чтобы гарантировать, что некоторые, ио ие все константные объекты можно разместить в постояииой памяти (ПЗУ), я принял следуюшее правило: любой обьект, имеющий коиструктор (то есть нуждающийся в инициализации во время выпал иеиия)„ие может размешаться в П ЗУ, а все остальные константные — могут. Это напрямую связано с вопросом о том, что, когда и как может быть инициализироваио (см.