Форум » C/C++ » Баги компиляторов MS VC++ 2010 и GCC 4,7.1: Магия С++: когда константа перестает быть константой » Ответить

Баги компиляторов MS VC++ 2010 и GCC 4,7.1: Магия С++: когда константа перестает быть константой

Сыроежка: Заголовок этой темы может натолкнуть на мысль, что речь пойдет об операторе приведения типов const_cast. Мол, какие проблемы: примени const_cast к константному объекту и тем самым уберешь константность. Вот и вся магия С++! На самом деле эта тема не посвящена оператору приведения типов const_cast, как это может изначально показаться из заглавия темы. Разговор пойдет совсем о другой "магии С++", которая порой ставит в тупик даже опытных программистов. Но раз const_cast все же упомянут, то он заслуживает, чтобы о нем было сказано несколько слов. Чаще всего оператор приведения типов const_cast используется для удаления константности у переменной, когда заранее известно, что объект, который она обозначает, не определен на самом деле константным. Попытка удалить константность у константного объекта с целью изменить его может привести к печальным последствиям, так как такое поведение неопределено. Вот что говорит по этому поводу стандарт С++ в параграфе №4 раздела "7.1.6.1 The cv-qualifiers": [quote]"4 Except that any class member declared mutable (7.1.1) can be modified, any attempt to modify a const object during its lifetime (3.8) results in undefined behavior." [/quote] Поэтому не пытайтесь это делать. Тем не менее это не означает, что оператор const_cast не находит применения. Примером использования оператора приведения типов const_cast может служить описание из книги Скотта Майерса "Эффективное использование С++. 55 верных советов" В "Правиле 3" в подразделе озаглавленном "Как избежать дублирование в константных и неконстантных функциях-членах" Майерс демонстрирует, как можно не дублировать код в реализации оператор-функций индексации. Обычно в классах объявляются две такие функции. Одна из них возвращает константную ссылку на объект и объявляется константной, другая же возвращает неконстантную ссылку на объект, тем самым позволяя менть значение объекта. Обычно эти две функции за исключением квалификаторов const имеют одинаковый код, поэтому одну из функций можно реализовать простым вызовом другой функции. Естественно в этом случае придется применить приведение типов const_cast, чтобы убрать константность. Допустим уже имеется определение константной оператор-функции индексирования для некоторого объекта типа char с именем text в класса SomeClass: (символьного массива) [pre2]const char & operator []( std::size_t pos ) const { /* возмодно некоторые необходимые операторы *. return ( text[position] ); }[/pre2] Тогда неконстантную оператор-функцию можно реализовать просто через вызов константной оператор-функции: [pre2]char & opertaor []( std::size_t pos ) { return ( const_cast<char &>( static_cast<const SomeClass &>( *this )[position] ) ); }[/pre2] В этой реализации неконстантной оператор-функции оператор приведения типов const_cast относится к возвращаемому типу значения функции, чтобы исключить константность, которая присутствует у возвращаемого типа значения константной оператор-функции, которая вызывается. static_cast, напротив, применяется к исходному объекту, чтобы добавить ему константность, так как константную функцию класса можно вызывать только для константных объектов. Вот и весь фокус реализации неконстантной оператор-функции через вызов ее близнеца - константной оператор-функции. Отмечу, что Стандарт С++ запрещает использовать static_cast для удаления констатности объяекта. Вот соответствующая выдержка из параграфа №1 раздела стандарта С++ 5.2.9 Static cast [quote]The static_cast operator shall not cast away constness[/quote] Ради справедливости отмечу, что в исходном примере static_cast используется не для удаления константности объекта, а для противоположной по значению операции: придания константности объекту. На этом можно поставить точку в разговоре об операторе приведения типов const_cast, так как главный вопрос темы, та магия С++, о которой я хочу рассказать, относится совсем не к этому оператору.

Ответов - 5

Сыроежка: Если в определении класса вам нужно использовать некоторые константы, то вы можете воспользоваться объявлением статических констант внутри класса, присвоив им при объявлении требуемые значения. Такая инициализация статических констант при их объявлении допустима для целочисленных, или имеющих тип перечисления констант. Об этой предоставляемой возможности языка С++ говорится в парраграфе №3 раздела 9.4.2 Static data members стандарта С++: 3 If a non-volatile const static data member is of integral or enumeration type, its declaration in the class definition can specify a brace-or-equal-initializer in which every initializer-clause that is an assignment expression is a constant expression (5.19). Итак, допустим, в вашем классе имеются объявления двух статических констант типа int, которые условно можно назвать min и max, и есть две статические функции, которые возвращают тзначения этих констант. Ниже приведен примерный код, демонстрирующий сказанное. [pre2] #include <iostream> class A { public: static int get_min() { return ( min ); } static int get_max() { return ( max ); } private: static const int min = 0; static const int max = 100; }; int main() { std::cout << "A::min = " << A::get_min() << std::endl; std::cout << "A::max = " << A::get_max() << std::endl; return ( 0 ); }[/pre2] Этот код должен компилироваться любым компилятором, который поддерживает стандарт С++, начиная со стандарта С++ 2003 года. Вы довольны, ваш код компилируется, объявление класса достаточно наглядное и компактное, так как значения констант заданы внутри класса, и вам нет необходимости их определять в каком-нибудь отдельном программном модуле. Все определения класса могут распологаться в одном заголовочном фвйле. Поэтому если надо будет делать какие-то изменения в определении класса, то не надо будет искать определения его компонент, разбросанных по многочисленным файлам. Предположим, что свой проект вы собираете с помощью компилятора MS VC++ 2010. Через некоторое время работы с проектом вам пришла замечательная идея написать всего лишь одну функцию доступа к вашим константам, в которой выбор требуемой константы будет осуществляться с помощью параметра функции и условного (тернарного) оператора. Для простоты этот параметр будет иметь тип bool. Если в качестве аргумента при вызове функции указывается значение false, то возвращается минимум, а если true, то возвращается максимум. Вот как будет выглядеть после проделанных изменений код вашей программы: [pre2] #include <iostream> class A { public: static int get_limit( bool bound ) { return ( ( bound ) ? max : min ); } private: static const int min = 0; static const int max = 100; }; int main() { std::cout << "A::min = " << A::get_limit( false ) << std::endl; std::cout << "A::max = " << A::get_limit( true ) << std::endl; return ( 0 ); }[/pre2] Вы протестировали свою модифицированную программу с помощью MS VC++ 2010, убедились в ее работоспособности и передали в промышленную эксплуатацию. Все было хорошо до тех пор, пока вам не потребовалось перенести свою программу на другую платформу, где используется компилятор GCC 4.7.1. Так как ваша программа достаточно простая, и вы убедились, что она успешно работает, будучи скомпилированной компилятором MS VC++ 2010, и никаких "подводных камней" в ее коде вы не усматриваете, то уверены, что перенос на новую платформу вашей программы займет считанные минуты - время, которое потребуется на ее компиляцию компилятором GCC 4.7.1. Итак, вы запускаете свою программу на компиляцию компилятором GCC 4.7.1, будучи совершенно спокойным и уверенным в успехе, и...о чудо, компилятор GCC 4.7.1 неожиданно для вас сообщает об ошибках компиляции! undefined reference to `A::max undefined reference to `A::min Вы смотрите в прострации на эти соообщения об ошибках и не верите своим глазам! Вы же ничего не меняли в программе, даже не прикосались к ее исходному коду. Почему компилятор говорит, что A::min и A::max не определены, когда, вот, они перед вашими глазами присутствуют в вашем классе?! Вы думаете, что может быть случайно задели какую-то клавишу, которая привела к опечатке, и заново аккуратно набираете в классе имена этих констант. Пускаете программу на компиляцию, но ничего не меняется. Компилятор настойчиво сообщает об одних и тех же ошибках компиляции, что имена A::min и A::max.не определены в программе. Никакого иного объяснения в вашу голову не приходит кроме заключения о том, что компилятор GCC 4.7.1 имеет баг! Или... может быть это MS VC++ 2010 имеет баг?! Вы долго смотрите на свой код, и у вас создается такое впечатление, что совершилась какая-то необъяснимая магия, как будто некий фокусник у вас на глазах украл ваши константы, и как это произошло, вы объяснить не в состоянии.

Сыроежка: Возникает три вопроса. 1. Имеет ли место баг компилятора GCC 4.7.1? 2. Имеет ли место баг компилятора MS VC++ 2010? 3. Как повлияло использование условного (тернарного) оператора на поведение компиляторов, ведь первоначальная версия класса без использования тернарного оператора успешно компилировалась обоими компиляторами? Очевидно, что следует начать с ответа на последний третий вопрос. Из него будет ясно, действительно ли какой-либо из указанных компиляторов имеет баг, или может быть здесь, к примеру, имеет место тот случай, когда в результате некорректного использования конструкций языка, поведение программы становится неопредленным. Правда, не понятно, что в этом простом коде является некорректным использованием конструкций языка?!

Сыроежка: Не стоит конечно терять самообладание и становиться мнительным, стараясь найти в каждой простой конструкции "двойное дно". Условный оператор в функции get_limit использован совершенно корректно. Тогда в чем же дело? Обратимся к тексту сообщений об ошибке компилятора GCC 4.7.1. Он гласит, что имеет место ссылка на неопределенные имена A::min и A::max. Почему бы тогда не определить их таким же образом, как определяются статические члены данных классов? Измененный код программы с включением дополнительных определений статических переменных A::min и A::max будет выглядеть следующим образом: [pre2] #include <iostream> class A { public: static int get_limit( bool bound ) { return ( ( bound ) ? max : min ); } private: static const int min = 0; static const int max = 100; }; const int A::min; const int A::max; int main() { std::cout << "A::min = " << A::get_limit( false ) << std::endl; std::cout << "A::max = " << A::get_limit( true ) << std::endl; return ( 0 ); } [/pre2] Запускаем код на компиляцию с помощью компилятора GCC 4.7.1, и к радости убеждаемся, что код успешно компилируется и выполняется. Но почему в первоначальной программе без дополнительного определения статических переменных A::min и A::max код успешно компилировался обоими компиляторами? Значит все-таки дело именно в условном операторе? Да, это так. Осталось только выяснить, почему использование условного оператора в этой простой программе изменило семантику конструкцмй языка С++.


Сыроежка: Обратимся снова к параграфу №3 раздела 9.4.2 Static data members стандарта С++. Там есть важное дополнение: The member shall still be defined in a namespace scope if it is odr-used (3.2) in the program В параграфе №2 раздела 3.2 One definition rule дается пояснение того, что означает odr-used: A variable whose name appears as a potentially-evaluated expression is odr-used unless it is an object that satisfies the requirements for appearing in a constant expression (5.19) and the lvalue-to-rvalue conversion (4.1) is immediately applied. Рассмотрим функцию get_limit, слегка изменив ее тело посредством добавления ссылки на результат выполнения условного оператора. [pre2] static int get_limit( bool bound ) { const int &r = ( bound ) ? max : min; return ( r ); }[/pre2] Чтобы проинициализировать константную ссылку r, она должна ссылаться на действительный объект. Об этом говорится в параграфе №5 раздела 8.3.2 References стандарт С++: A reference shall be initialized to refer to a valid object or function. Теперь процитируем параграф №4 раздела стандарта об условном операторе 5.16 Conditional operator: 4 If the second and third operands are glvalues of the same value category and have the same type, the result is of that type and value category Итак, чтобы инициализировать ссылку в функции get_limit обе переменные A::min и A::max должны представлять собой действительные объекты, а потому следует их определить в пространстве имен, внутри которого содержится объявление класса. Даже если убрать введенную дополнительно промежуточную ссылку r из функции и вернуться к ее первоначальному виду, все равно при выполнении условного оператора не происходит немедленного преобразования lvalue в rvalue, как того требует параграф №2 раздела 3.2 One definition rule для константных выражений, которые не являются odr-used. Условный оператор в С++ возвращает ссылку для данного случая, а потому для этой ссылки должен существовать действительный объект. В Комитет по стандартизации было направлено предложение по изменению стандарта, которое позволяло бы для констант, используемых в условном операторе, оставаться не odr-used. Это предложение имеет номер 712. и озаглавлено как Are integer constant operands of a conditional-expression "used?" Можно почитать об этом предложении, поданным в Комитет по стандартизации, по следующей ссылке Лично я против того, чтобы вносили данное изменение в стандарт, так как это нарушает различие в семантике условного оператора в С++ по сравнению с С. Из рассмотренного материала теперь легко можно сделать вывод, что именно компилятор MS VC++ 2010 содержит баг. Он не должен был компилировать код, а должен был потребовать явного определения статических констант класса. Вы возможно уже заметили, что в заголовке этой темы говорится о багах обоих компиляторов: и MS VC++ 2010, и GCC 4.7.1. Так где же баг компилятора GCC 4.7.1? Здесь имелся в виду баг, относящийся к другой синтаксической конструкции, но также тесно связанной с объявлением статических переменных в классе. Согласно параграфу №4 уже ранее цитируемого раздела стандарта С++ 9.4.2 Static data members "Unnamed classes and classes contained directly or indirectly within unnamed classes shall not contain static data members." Однако если попробовать скмопилировать следующий код [pre2] struct { const static int i = 10; int a[ i ]; } s; int main() { }[/pre2] то он успешно скомпилируется обоими компиляторами: и MS VC++ 2010, и GCC 4.7.1, чего не должно быть, как это следует из процитированного положения стандарта С++. Лично я не вижу причин, почему нельзя использовать статические константные члены данных в неименованных классах, если эти статические константные члены данных не являются odr-used, то есть они еще на этапе компиляции заменяются своими значениями. Поэтому я обратил внимание Комитета по стандартизации, что этот параграф стандарта следует изменить, разрешив в неименованных классах использовать объявления статических константных членов данных, если они не являются odr-used.

Сыроежка: В заключение хотел бы обратить внимание на допускаемый многими программистами ляп, когда они пытаются объявить константный статический объект типа double и инициализировать его во время объявления внутри класса. Например, [pre2] struct A { const static double middle = 50.0; };[/pre2] Увы, этого делать нельзя, так как только константные статические объекты, имеющие либо целочисленный тип, либо тип перечисления, можно инициализировать при их объявлении внутри класса. Поэтому любой компилятор, удовлетворяющий стандарту С++, должен выдать сообщение об ошибке для данного объявления структуры A. Тем не менее есть очень простой выход из данной ситуации. Нужно просто статический объект, имеющий тип числа с плавающей запятой, объявить не с квалификатором const, а со спецификатором constexpr, и тогда этот объект можно инициализировать при объявлении внутри класса. [pre2] struct A { constexpr static double middle = 50.0; };[/pre2] Этот код будет успешно компилироваться, если компилятор поддерживает спецификатор constexpr. Это еще одна магия языка С++!



полная версия страницы