пятница, августа 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 комментариев:

  1. Анонимный8/8/08 17:34

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

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

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

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

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

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

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

    ОтветитьУдалить
  5. Вообще-то да. Если мы определим

    typedef int BlaBlaBla;

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

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

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

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

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

    char* a, b, c;

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

    LPCHAR a, b, c;

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

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

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

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

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

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

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

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

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

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

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

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

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

    ОтветитьУдалить
  13. Всегда предпочитаю вместо const T* писать T const*

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

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

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

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

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

    ОтветитьУдалить
  16. Анонимный14/8/08 15:52

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

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

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

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

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

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

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

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

    ОтветитьУдалить