|
Программирование >> Полиморфизм без виртуальных функций в с++
f(1,2.0) f(1.0,2) f(z,1.2) f(int,double) f(double,int) f(complex,int) между переданными при вызове типами формальных и фактических аргументов. Предпочтительными считались такие соответствия, при которых выполнялись наименее опасные преобразования. Это позволило избежать противоречий со стандартными правилами расширения типов и старгдартными преобразованиями в С. В [Stroustrup, 1989b] для версии 2.0 данная схема описана так: ...за исключением немногих случаев, когда старые правило могли зависеть от порядка, новые совместимы, и ранее написанные программы дадут те же результаты, что и до введения новых правил. Компиляторы С++, выпущенные за последние два года, выдавали предупреждения по поводу тех зависящих ат порядка разрешений неоднозначности, которые теперь объявлены вне закона . В С++ различаются пять видав соответствий: □ не требующее никаких или требующее только неизбежных преобразований (например, имени массива в указатель, имени функции в указатель но функцию и Т в const т); □ нуждающееся в расширении интегральных типов (в соответствии са стандартом ANSI С, то есть: char в int, short в int и их unsigned аналогов), а также float в double; □ требующее стандартных преобразований (int в double, unsigned int в int, derived* в base* и т.п.); □ работающее с определенными пользователем преобразованиями (конструкторами и конверторами); □ учитывающее мнагатачия (...) в объявлении функции. Рассмотрим сначала функции с одним аргументом. Всегда нужно выбирать лучшее соответствие, а именно то, которое расположено выше в приведенном списке. Если есть два лучших соответствия, то вызов признается неоднозначным и компилятор выдает ошибку . Приведенные выше примеры иллюстрируют правило. Более точное его изложение можно найти в ARM. Следующее правило необходимо соблюдать, когда функция имеет более одного аргумента [ARM]: Если при вызове передается более одного аргумента, то выбирается функция, у которой хотя бы для одного из них соответствие лучше, а для остальных не хуже. Например: class complex { . . . complex(double); f(z,l,3); f(complex ...) f(2.0,z); f(int ...) f(l,l); ошибка: неоднозначность f(int,double) или f(double,int) Нежелательное сужение типа double до int в третьем и предпоследнем вызовах влечет 30 собой предупреждение компилятора. Такое сужение розрешено в целях сохранения совместимости с С. В данном случае сужение безвредно, но во многих других может привести к искажению значения, поэтому необоснованно применять его не стоит . Уточнение формулировки этого правила для нескольких аргументов ггривело к появлению правила пересечения [ARM, стр. 312-314]. Впервые его сформулировал Эндрю Кениг в беседе с Дугом Макилроем, Джонатаном Шопиро и мной. Кажется, именно Шопиро обргаружил странные примеры, доказавпше необходимость этого правила [ARM, стр. 313]. Обратите внимание на то, как серьезно мы подходили к вопросам совместимости. Если бы, принимая проектные решения, мы систематически отдавали предпочтение простоте и изяществу, а ire совместимости, то сегодня С++ был бы куда ко.мпактнее и чище. Но в то же вре.мя он был бы языком, интересны.м лишь малой горстке ценителей. 11.2.3. Нулевой указатель Кажется, ничто не вызвало более жарких споров, чем вопрос о том, как правильно обозначить нулевой указатель (пе указывающий ни на какой объект). С++ унаследовал определение пулевого указателя от классического С [Kernighan, 1978]: Константное выражение со значением О преобразуется в указатель, обычно называемый нулевым. Гарантируется, что такой указатель можно отличить от указателя но любой объект или функцию . ARM дополнительно предупреждает: Отметим, что нулевой указатель не обязательно представляется той же комбинацией битов, что и целый О . Это предостережение против распространенного заблуждения, что если выражение р=0 делает указатель р нулевым, то представление нулевого указателя должно быть в точности таки.м, как у целого нуля, то есть состоять из одних нулевых битов. Но С++ достаточно сильно типизирован, для того чтобы позволить разработчику компилятора выбрать для нулевого указателя любое представление, независимо от того, как он выглядит в исходно.м тексте программы. Одно из исключений - использование многоточия для подавления контроля аргументов функции: int printf(const char* . . .) ; неконтролируемый вызов в стиле С printf(fmt. О, (char)O, (void*)0, (int*)0, (int(*) ())0); Здесь необходи.мы приведения типов для явного указания, какой О требуется. В это.м примере могло бы быть передано пять разных значений. В K&R С аргументы функции вообще не проверялись, и даже в ANSI С нельзя полагаться на контроль аргументов, поскольку он необязателен. Из-за этого обнаружить все нули в програм.ме на С или С-1-+ нелегко. А поскольку в других языках принято использование символической константы для представления пулевого указателя, то программисты на С традиционно применяли для этой цели макрос NULL. К сожалению, в K&R С нет корректного переносимого определения NULL, в ANSI С разумны.м и все чаще употребляюпп1мся определением NULL является (void*) 0. Однако в С++ и (void*) О нельзя счесть удачным обозргачение.м нулевого указателя: char* р = (void*)0; корректно в С, но ке в С++ Указатель void* нельзя присвоить ничему без явного приведения типа. Если разрешить неявное преобразование void* к друго.му типу указателя, то появится серьезная прореха в системе типов. Можно было бы считать (void* ) О специальным случаем, но вводить его стоит только тогда, когда ничего другого не остается. Кроме того, порядок применения С++ был определен задолго до принятия стандарта ANSI С, и я ие хочу, чтобы особо важная часть С++ зависела от макросов (см. главу 18). Поэто.му я использовал просто 0. Те, кто не может жить без символической константы, обычно определяют нечто похожее на const int NULL = 0; или #define NULL О Для компилятора определенный таким образом NULL и О - синонимы. К сожалению, многие включили в свой код определения NULL, NIL, Null, null и т.д., так что вводить еще одно просто опасно. Есть одна ошибка, которую невозможно обнаружить, если О (как бы он ин был назван) используется в качестве нулевого указателя. Рассмотрим пример: void f (char*); void gO { f(0); } вызывает f(char*) Если добавить еще одну функцию f (), то семантика g () изменится без предупреждения: void f(char*); void f (int); void g() { f(0); } вызывает f(int) Этот неприятный побочный эффект возникает из-за того, что О - это значе-1гие типа int, которое может быть расширено до нулевого указателя, а не пря.мое обозначение последнего. Думаю, что хороший компилятор .мог бы выдать предупреждение, но во вре.мя работы над Cfront я об этом не поду.мал. Можно было бы считать вызов f (О) неоднозначным, а не разрешать его в пользу f (int), но это могло бы не понравиться тем, кто хочет видеть особый смысл в NULL или nil.
|
© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки. |