Б. Страуструп - Дизайн и Эволюция C++. 2006 (1160775), страница 39
Текст из файла (страница 39)
Вот другой пример: с1авв Т ( А Й[); уорд() (Ла; /* ... */ ) гуреев гпг. А; ): 6.3.12. Зачем допускать опережающие ссьшки? Видимо, всех этих проблем можно было бы избежать, если настоять на применении только однопроходных анализаторов. Имя можно использовать только тогда, когда оно объявлено «выше/раньше», а то, что происходит «ниже/позже», не влияет на зто объявление. В конце концов, именно это правило применяется в С и в С++. Например: 1пс х; уоЫ г() ( спс у=х; 1пс х=7; гпг х=х; ) // глобальная х // локальная х Однако, когда я только проектировал классы и встраиваемые функции, Дуг Макилрой убедительно возразил, что применение данного правила к объявлениям классов может привести к путанице.
Например: гпс х; с1авв х ( уова 1() ( 1пг у = х ) //::х иян Х::х7 Зб д(); 1ог х; Предположим, что тип А не был определен вне Т. Законно ли объявление т:: б ()? А определение т::д ()? В АКМ объявление т:: Е () считается незаконным, поскольку тип А в этой точке не определен.
Это не вызывает противоречия с АА)51/130. С другой стороны, согласно АКМ, определение д() законно, если правило переписывания интерпретировать в том смысле, что переписывание производится до начала синтаксического анализа, и незаконно, если допустить, что сначала выполняется синтаксический анализ, а затем — переписывание. Проблема заключается в том, является ли А именем типа к моменту начала анализа. Я полагаю, что в АКМ выражена первая точка зрения (то есть объявление т:: д ( ) законно), но не стал бы утверждать, что это бесспорно.
ПИИИИИИВ Стандартизация уо1бб() ((пгу=х; )//Х::х ); 6.3ЛЗ. Правила разрешения имен АИ51/150 С годами мы нашли много примеров, для которых явных правил АКМ было недостаточно. Зависимость от порядка в пих была совершенно неочевидной и потенциально опасной, а интерпретация правил — неоднозначной. Один из интересных примеров нашел Скотт Тэрнер: Гуредег гп Р(); гуреде 1пг ()(): с1азз Х ( всаг1с Р(0); О определим О как Р // эквивалентно "вгас1с 1п ()()" // скобки, в которые взят О, избыточны // Я в этой области действия уве не тип всат(с Ц(Р); // определим Ц как функцию, принимавшую аргумент // типа Р и возвращающую 1пс.
// эквивалентно "вгасфс (пс 9(1пг())" ); Объявление двух функций с одинаковыми именами в одной и той же области действия допустимо, если типы их ар)ументов достаточно различаются. При изменении порядка объявления членов мы получим две функции с именем Р. Удалим бурею(еб для Р или для () и получим новую семантику. Заметим, что этот пример, как и многие другие, основан на злосчастном правиле «неявного 1пс», унаследованном от С. Еще более десяти лет назад я пытался избавиться от него 1см. раздел 2.8.1).
Но, увы, есть примеры и другого рода: 1пг Ь; с1авв Х ( вгаг1с 1пг а[втеео1(Ь)); всаг1с 1пг Ь[з1зеог(а)); ио)с1 х::и() ( 1пг у = х; ) // х::х Если объявление х велико, то присутствие различных х часто будет оставаться незамеченным. Более того, если член х используется не очень последовательно, то изменение порядка членов приведет к молчаливому изменению семантики. Вынесение определения функции за пределы объявления класса также может повлечь за собой изменение семантики без предупреждения. Правила переписывания и переопределения зап(ищали от подобных коварных ошибок и обеспечивали некоторую свободу при реорганизации классов.
Эти аргументы применимы не только к классам, но издержки компилятора на обеспечение такой защиты приемлемы лишь для классов, и лишь для классов можно избежать проблем совместимости с С. Кроме того, именно в объявлениях классов псреупорядочение делается чап(е всего и как раз там оно может дать нежелательные побочные эффекты. Как велась работа ЛИИИИИИИ3 Данный пример содержит ошибку, поскольку Ь изменяет смысл после использования. К слову сказать, такие ошибки компилятору обнаружить легко, нс то что в случае с Р (0) .
В марте 1993 г. в Портленде комитет одобрил следующие правила: о область действия имени, объявленного в классе, состоит це только из текста, следуюшего за объявителем, но также из определений всех функций, аргументов по умолчанию и инициализаторов конструкторов, которые встречаются в атом классе (включая и вложенные в него классы). Собственно имя класса в эту область не входит; а имя, использованное в классе Я, должно относиться к одному и тому же объявлению независимо от того, вычисляется оцо в своем контексте или в полной области действия Я.
Полная область действия Я состоит из самого класса Я, его базовых классов и всех классов, объемлющих Я. Часто это правило называют «правилом пересмотра» (Йе гссопзЫегаг)оп гп1е); ш если изменение порядка следования объявлений членов дает новую, но корректную программу с учетом правил 1 и 2, семантика программы не определена. Часто это называют «правилом переупорядочения» (1йе геог()ег)пя гп1е), Отмечу, что лишь очень немногие программы затронуло такое изменение правил. Новые установки — это лишь более четкая формулировка первоначальных намерений.
На первый взгляд, они требуют многопроходного алгоритма при реализации С++. Но их можно реализовать за один проход по тексту, за которым слсдуют один илн несколько просмотров собранной на атом проходе информации. Производительность при этом почти не меняется. б.3.2. Время жизни объектов Для многих операций в С++ приходится создавать временные значения. Например: »о1ц 1(х а1, х а2) ( ехсегп той д(сопес ха); х ю з = а1«а2; П(а1+а2); // ) Обычно для хранения результата а1»а2 перед присваиванием его переменной з необходим объект (вероятно, типа х). Точно так же нужен объект, в который по- мешается сумма а1»а2 перед передачей функции д ( ) . Предположим, что в классе х есть деструктор.
В какой момент он должен вызываться для временного объекта? Первоначально ответ на этот вопрос звучал так: «в конце блока, точно так же, как для любой другой локальной переменнойм Но оказалось, что тут есть две проблемы: а иногда при этом объект уничтожается слишком рано. Например, д ( ) может поместить указатель на свой аргумент (временный объект, в котором ПИИИИИИВ Стандартизация човй Г(Х а1, Х а2) ( ехсегп ~оЫ д(сопев Хэ); Х к; // (в = а1+а2;) (д(а1+а2);) // Если точка уничтожения расположена в конце блока (в Сггопг так оно и есть), то у пользователей хотя бы остается возможность явно обойти проблему.
Однако некоторые требовали более удобного ее решения. Поэтому в АКМ это правило было ослаблено, и объект мог теперь уничтожаться в любой точке между первым его использованием и концом блока. Но, поскольку в разных компиляторах время жизни объекта устанавливалось по-разному, ситуация не стала лучше. Поэтому невозможно было написать гарантированно переносимую программу, если только не предполагать, что временный объект уничтожается немедленно.
Это решение быстро признали неприемлемым, поскольку оно было несовместимо с некоторыми общеупотребительными в С идиомами, например: с1авв ЯШпд ( // риь11с: гг1епо Ясг1пд орегасог+(соево яггьпдв, попас Ясгтпдэ); // орегасог сопев сваг*()/ // С-строка ); ~оЫ Г(ЯСг1пд в1, Ягг1пд в2) ( рг1псг("вв", (попас спас*)(в1+в2))2 // ) хранится сумма а1+а2) в стек, а какая-то другая функция попытается затем воспользоваться им, сняв его со стека уже после того, как произошел возврат нз й ( ) и, стало быть, временный объект уничтожен; с) иногда временный объект живет слишком долго. Например, что произойдет, если х — тип матрицы размером 1000к1000, а в блоке создается несколько десятков временных матриц7 Так можно исчерпать даже довольно большую физическую память и заставить механизм виртуальной памяти постоянно подкачивать страницы.
По-моему, в реальных программах первая проблема встречается редко и общее решение дает автоматическая сборка мусора (см. раздел 10.7). Но вот вторая весьма распространена и серьезна. Некоторые пользователи вынуждены решать ее, заключая в отдельный блок каждое предложение, в котором есть вероятность создания временных объектов: Как велась работа 16ИИИИИИИИ човй д(эсгвпд в1, Ясгьпд в2) ( сопев сваг* р = в1+в2; ргвпвс("«в",р); О При использовании стратегии «уничтожения в конце предложения» С-строка, на которую указывает р, оказалась бы во временном объекте, представляюшем в1+в 2, и была бы уничтожена в конце предложения инициализации р. Дискуссии о продолжительности жизни временных объектов не затихали в комитете примерно два года, пока Дэг Брюк (Рая Вгьс)с) не положил им конец.
Перед этим комитет потратил массу времени в спорах о сравнительных достоинствах тех или иных решений. Все соглашались, что ни одно из них не совершенно. Мое мнение, выраженное довольно резко, заключалось в том, что «промедление смерти подобно», поэтому хоть какому-то решению нужно отдать предпочтение. Я думаю, что был выбран наилучший вариант. В июле 1993 г.
Дэг Брюк представил отчет о состоянии дел по этому вопросу, основанный главным образом на работе Эндрю Кенига, Скотта Тэрнера и Тома Пеннелло. В нем были определены семь вариантов установления точки разрушения временного объекта: (э сразу после первого использования; 0 в конце предложения; о в следующей точке ветвления; о в конце блока (оригинальное правило С++, реализованное в С(гопс); (з в конце функции; о после последнего использования (неявно подразумевается сборка мусора); а где-то между первым использованием и концом блока (правило АВМ). Для создания С-строки, передаваемой для печати функции ргупс1, используется конвертор из класса асс(пд.
В типичной реализации конвертор просто возвращает указатель на часть объекта ясг1пд. При такой простой реализации конвертора этот пример не будет работать, если применяется «немедленное уничтожение временных объектов». Происходит вот что: создается временный объект для хранения в1+в2, указатель на внутреннюю область этого объекта передается конвертору в С-строку, временный объект уничтожается, а уже после этого указатель на уничтоженный объект передается рг(пс й ( ) . Но деструктор временного объекта класса Я с г(пд освободил память, где находилась С-строка.