пятница, августа 08, 2008

typedef и const

Рассказ о типичной ошибке, связанной с typedef и const. Никогда на это не попадалась, к счастью...

typedef - это не простая подстановка. Поэтому некоторые моменты, связанные с typedef'ом, могут удивить.
Допустим, в коде объявлены какие-то такие переменные.

const char* pX, Y, Z; //1

Через некоторое время мы решаем использовать typedef.
typedef char* pChar; //2

И после этого переписываем первый кусок кода как
const pChar pX, Y, Z; //3

Что неправильно, потому что первый и третий код не эквивалентны. Третий код без typedef'а будет выглядеть так.
char * const pX, * const Y, * const Z; 

Тут не только Y и Z совсем не того типа, которого хотелось, но и у pX const применен к самому указателю, а не к тому, на что тот указывает. Всё это потому, что здесь
const pChar pX, Y, Z;

мы объявили pX, Y и Z константами типа pChar, а это не эквивалентно замене char* другим словом.

Поэтому в winnt.h, есть typedef'ы как для неконстантных вариантов указателей, так и для константных.

typedef char* LPSTR;
typedef const char* LPCSTR;
(реально там объявления более лохматые, но смысл такой)

В статье Const input parameters and typedefs нам советуют избегать typedef'ов для типов с указателями и использовать const только явно и когда он нужен.

Пример кода я взяла отсюда:
comp.lang.c - typedef const char*

17 коммент.:

Анонимный комментирует...

Кстати, об этом очень подробно писалось в книге Expert C Programming (автор Peter van der Linden). Книга полна мелких неожиданностей, разборов заблуждений и неплохого юмора.

Sergey Kishchenko комментирует...

У вас несколько существенных ошибок. Во-первых, в первой строке const относится ко всем переменным, а неинициализированные константы - это ошибка. Во-вторых, не забываем, что указатель в первой строке относится только к первой переменной. Таким образом, замена char* на pChar даже без const неправомерна - тогда все три переменные станут указателями. Ограничьтесь одной переменной для примера :)

Yuri Volkov комментирует...

typedef по-моему работает как алиас, т.е. вводит новое имя для типа, но не определяет его =)

Alena комментирует...

Сергей Кищенко:
У вас несколько существенных ошибок. Во-первых, в первой строке const относится ко всем переменным, а неинициализированные константы - это ошибка.

Не хочу загромождать код инициализацией, смысл происходящего в общем ясен.

Во-вторых, не забываем, что указатель в первой строке относится только к первой переменной. Таким образом, замена char* на pChar даже без const неправомерна - тогда все три переменные станут указателями.

Угу, собственно речь и об этом тоже.

Unknown комментирует...

Вообще-то да. Если мы определим

typedef int BlaBlaBla;

то впоследствии сможем использовать BlaBlaBla там, где должен быть int и наоборот.
Как и сказал Юрий, это всего лишь дополнительный алиас.

Alena комментирует...

2Yuriy Volkov:
typedef по-моему работает как алиас, т.е. вводит новое имя для типа, но не определяет его =)

И это правильно... изменила формулировку.

zrxq комментирует...

Да ну, детская совершенно ошибка. Решается "приклеиванием" звёздочки к имени идентификатора, а не к типу.

К тому же, обычно ошибаются в обратную сторону:

char* a, b, c;

...еще можно ошибочно воспринять как декларацию трёх указателей. Но

LPCHAR a, b, c;

...воспринять как декларацию одного указателя и двух статических - надо очень сильно постараться.

Или я опять чего-то не понял? =)

zrxq комментирует...

А, ясно. Статья по ссылке немного более внятная, т.к. нет ударения на самый очевидный косяк - "*" перед первым идентификатором и её отсутствие перед остальными.

Alena комментирует...

2zorgg:
Да ну, детская совершенно ошибка. Решается "приклеиванием" звёздочки к имени идентификатора, а не к типу.

Угу, можно еще в coding conventions это записать для верности...

Или я опять чего-то не понял? =)

Ну основное тут всё-таки, что const оказался не там где ожидалось.

Sergey Kishchenko комментирует...

Алёна:
Не хочу загромождать код инициализацией, смысл происходящего в общем ясен.
Просто вы описываете одну ошибку, а там их много. Кто-то прочтет, описанную ошибку не допустит, но зато допустит множество других.

zrxq комментирует...

Ну основное тут всё-таки, что const оказался не там где ожидалось.

Да, уже допёр. Кстати, странно, что я на эти грабли ни разу не наступил (при том, что никогда об этом не задумывался).

Alena комментирует...

2Сергей Кищенко:
Просто вы описываете одну ошибку, а там их много. Кто-то прочтет, описанную ошибку не допустит, но зато допустит множество других.

Ээээ... Ну если подойти к вопросу формально, то я говорю, что один код не эквивалентен другому. Про то, что они оба не компиляются речь не идет :-)

Сергей, я действительно считаю, что инициализация константы в данном случае - это фигня. Компилятор сразу даст внятную диагностику и проблем не возникнет...

CyberZX комментирует...

Всегда предпочитаю вместо const T* писать T const*

Raider комментирует...

typedef - это определение нового типа, а не простая подстановка.

новый тип typedef-ом не создается. проверить можно через typeid() и перегруженные шаблоны:

template <typename T> class x {};
template <> class x<bool> {};
typedef bool tbool;
template <> class x<tbool> {} // error

Alena комментирует...

2Raider:
новый тип typedef-ом не создается. проверить

Да, да, меня уже попинали. Не везде исправила, пропустила...

Анонимный комментирует...

На такие грабли, как по ссылке, не наступал - т.к. никогда не мог запомнить, к чему относится const при записи указателей (с typedef понятно, без - я впадаю в ступор).

Вообще, конечно, система описания типов в С++ - не супер... в том же C# более разумный подход.

Kodt комментирует...

В статье Const input parameters and typedefs нам советуют избегать typedef'ов для типов с указателями и использовать const только явно и когда он нужен.

Это называется дуть на воду, обжегшись на молоке.

Да, к сожалению, синтаксис в этом месте гибок до такой степени, что тупая макроподстановка или замена меняет смысл, не создавая синтаксических ошибок.

Но это не повод отказываться от const внутри typedef'ов.

Вообще я заметил, что проблемы с константностью вылезают тогда, когда есть проблемы с архитектурой. Если ты наперёд не знаешь, можно или нет модифицировать данный объект в данном месте, то тупо работаешь с ним через неконстантные ссылки/указатели/методы. И от этого места расползается волна неконстантности на всю программу.
В результате уже невозможно сказать - будет ли меняться состояние системы, или нет, в каком-то отдалённом месте.

Такой дурацкой практике здорово способствует COM, поскольку в IDL константность накладывается только на типы-значения (строки, структуры). А все объекты - вынь да положь неконстантность.