8.6 – typedef и псевдонимы типов
Ключевое слово typedef
В C++ typedef
(сокращенно от «type definition», «определение типа») – это ключевое слово, которое создает псевдоним для существующего типа данных. Чтобы создать такой псевдоним, мы используем ключевое слово typedef
, за которым следует существующий тип данных для псевдонима, за которым следует имя для псевдонима. Например:
typedef double distance_t; // определяем distance_t как псевдоним для типа double
По соглашению имена typedef
объявляются с использованием суффикса "_t". Это помогает указать, что идентификатор представляет собой тип, а не переменную или функцию, а также помогает предотвратить конфликты имен с другими типами идентификаторов.
Лучшая практика
Называйте свои псевдонимы typedef
с суффиксом _t
, чтобы указать, что это имя является псевдонимом типа, и чтобы помочь предотвратить конфликты имен с другими типами идентификаторов.
После определения имя typedef
можно использовать везде, где требуется тип. Например, мы можем создать переменную с именем typedef
в качестве типа:
distance_t milesToDestination{ 3.4 }; // определяет переменную типа double
Когда компилятор встречает имя typedef
, он подставляет тип, на который указывает typedef
. Например:
#include <iostream>
int main()
{
typedef double distance_t; //определяем distance_t как псевдоним для типа double
distance_t milesToDestination{ 3.4 }; // определяет переменную типа double
std::cout << milesToDestination << '\n'; // выводит значение double
return 0;
}
Этот код печатает:
3.4
В приведенной выше программе мы сначала определяем typedef distance_t
как псевдоним для типа double
.
Затем мы определяем переменную с именем milesToDestination
типа distance_t
. Поскольку компилятор знает, что distance_t
– это typedef
, он будет использовать тип, на который указывает псевдоним, то есть double
. Таким образом, переменная milesToDestination
фактически компилируется как переменная типа double
, и во всех отношениях она будет вести себя как double
.
Наконец, мы печатаем значение milesToDestination
, которое печатается как значение double
.
typedef
не определяет новый тип
Обратите внимание, что typedef
не определяет новый тип. Скорее, он просто создает новый идентификатор (псевдоним) для существующего типа. typedef
можно использовать как замену везде, где можно использовать обычный тип.
Это позволяет нам делать вещи, которые синтаксически корректны, но семантически бессмысленны. Например:
int main()
{
typedef long miles_t; // определяет miles_t как псевдоним для типа long
typedef long speed_t; // определяет speed_t как псевдоним для типа long
miles_t distance { 5 }; // distance на самом деле просто long
speed_t mhz { 3200 }; // mhz на самом деле просто long
// Следующее синтаксически корректно (но семантически бессмысленно)
distance = mhz;
return 0;
}
Хотя концептуально мы предполагаем, что miles_t
и speed_t
имеют разные значения, оба они являются просто псевдонимами для типа long
. Это фактически означает, что значения типа miles_t
, speed_t
и long
могут использоваться взаимозаменяемо. И действительно, когда мы присваиваем значение типа speed_t
переменной типа miles_t
, компилятор видит только то, что мы присваиваем значение типа long
переменной типа long
, и он не будет жаловаться.
Предупреждение
Следует проявлять осторожность, чтобы не смешивать значения псевдонимов типов, которые должны быть семантически разными.
В качестве отступления...
Некоторые языки поддерживают концепцию строгих псевдонимов типов (strong typedef). Строгий typedef
фактически создает новый тип, который имеет все исходные свойства исходного типа, но компилятор выдаст ошибку, если вы попытаетесь смешать значения типа, на который ссылается псевдоним, и строгого typedef
. На этапе C++20, C++ напрямую не поддерживает строгие псевдонимы типов (хотя классы перечислений, рассмотренные в уроке «9.3 – Классы перечислений», похожи), но существует довольно много сторонних библиотек C++, которые реализуют строгое поведение, похожее на typedef
.
Область видимости typedef
Поскольку область видимости является свойством идентификатора, идентификаторы typedef
подчиняются тем же правилам области видимости, что и идентификаторы переменных: typedef
, определенный внутри блока, имеет область видимости блока и может использоваться только внутри этого блока, тогда как typedef
, определенный в глобальном пространстве имен, имеет область видимости файла и может использоваться до конца файла. В приведенном выше примере miles_t
и speed_t
можно использовать только в функции main()
.
Если вам нужно использовать один или несколько typedef
в нескольких файлах, их можно определить в заголовочном файле и включить через #include
в любые файлы исходного кода, которые должны использовать это определение:
mytypes.h:
#ifndef MYTYPES
#define MYTYPES
typedef long miles_t;
typedef long speed_t;
#endif
Определения typedef
, включенные таким образом, будут импортированы в глобальное пространство имен и, следовательно, будут иметь глобальную область видимости.
Проблемы typedef
Однако у typedef
есть несколько синтаксических проблем. Во-первых, легко забыть, что на первом месте: имя типа или определение типа. Что правильно?
typedef distance_t double; // некорректно
typedef double distance_t; // корректно
Это легко перепутать. К счастью, в таких случаях компилятор пожалуется.
Во-вторых, синтаксис typedef
становится уродливым при использовании более сложных типов, особенно указателей на функции (которые мы рассмотрим в уроке «11.9 – Указатели на функции»):
typedef int (*fcn_t)(double, char); // fcn_t - идентификатор typedef
В приведенном выше определении typedef
имя нового типа (fcn_t
) скрыто в середине определения, что затрудняет чтение определения.
Псевдонимы типов
Чтобы помочь решить эти проблемы typedef
был добавлен улучшенный синтаксис, имитирующий способ объявления переменных. Этот синтаксис называется псевдонимом типа (type alias).
Например, следующий typedef
:
typedef double distance_t; // определяем distance_t как псевдоним для типа double
можно объявить как следующий псевдоним типа:
using distance_t = double; // определяем distance_t как псевдоним для типа double
Псевдонимы типов функционально эквивалентны определениям typedef
, но имеют преимущество в более удобном синтаксисе определения.
Вот трудно читаемый typedef
, который мы представили выше, вместе с эквивалентным (и немного более легким для чтения) псевдонимом типа:
typedef int (*fcn_t)(double, char); // fcn_t трудно найти
using fcn_t = int(*)(double, char); // fcn_t легче найти
Лучшая практика
При создании типов с псевдонимами отдавайте предпочтение синтаксису псевдонима (type alias) типа вместо синтаксиса typedef
.
Когда мы должны использовать псевдонимы типов?
Теперь, когда мы рассмотрели, что такое typedef
и псевдонимы типов, давайте поговорим о том, для чего они полезны.
Использование псевдонимов типов для кодирования, независимого от платформы
Одно из главных преимуществ псевдонимов типов заключается в том, что их можно использовать для скрытия деталей, специфичных для платформы. На некоторых платформах int
составляет 2 байта, а на других – 4 байта. Таким образом, при написании кода, независимого от платформы, использование int
для хранения более 2 байтов информации может быть потенциально опасным.
Поскольку char
, short
, int
и long
не указывают их размер, кроссплатформенные программы довольно часто используют псевдонимы типов для определения псевдонимов, которые включают размер типа в битах. Например, int8_t
будет 8-разрядным целым числом со знаком, int16_t
– 16-разрядным целым числом со знаком, а int32_t
– 32-разрядным целым числом со знаком. Подобное использование псевдонимов типов помогает предотвратить ошибки и дает более четкое представление о том, какие предположения были сделаны относительно размера переменной.
Чтобы гарантировать, что каждый псевдоним типа преобразуется в тип нужного размера, псевдонимы типа такого рода обычно используются в сочетании с директивами препроцессора:
#ifdef INT_2_BYTES
using int8_t = char;
using int16_t = int;
using int32_t = long;
#else
using int8_t = char;
using int16_t = short;
using int32_t = int;
#endif
На машинах, где int
составляет всего 2 байта, с помощью #define
может быть определен INT_2_BYTES
, и программа будет скомпилирована с верхним набором псевдонимов типов. На машинах, где int
равен 4 байтам, оставление INT_2_BYTES
неопределенным приведет к использованию нижнего набора псевдонимов типов. Таким образом, int8_t
будет преобразовываться в 1-байтовое целое число, int16_t
будет преобразовано в 2-байтовое целое число, а int32_t
будет преобразовано в 4-байтовое целое число, используя комбинацию char
, short
, int
и long
, которая подходит для машины, для которой компилируется программа.
Целочисленные типы фиксированной ширины (такие как std::int_fast16_t
и std::int_least32_t
) и size_t
(описанные в уроке «4.6 – Целочисленные типы фиксированной ширины и size_t
») – на самом деле псевдонимы для различных базовых типов!
Вот почему, когда вы печатаете 8-битное целое число фиксированной ширины с помощью std::cout
, вы, скорее всего, получите символьное представление. Например:
#include <cstdint> // для целочисленных типов фиксированной ширины
#include <iostream>
int main()
{
std::int_least8_t x{ 97 }; // int_least8_t на самом деле является псевдонимом типа для char
std::cout << x;
return 0;
}
Эта программа печатает:
a
Поскольку std::int_least8_t
обычно определяется как псевдоним типа для одного из типов char
, переменная x
будет определена как типа char
. А типы char
печатают свои значения как символы ASCII, а не как целые числа.
Использование псевдонимов типов для упрощения сложных типов
Хотя до сих пор мы имели дело только с простыми типами данных, в продвинутом C++ типы могут быть сложными и длинными для ввода. Например, вы можете увидеть функцию и переменную, определенные следующим образом:
#include <string> // для std::string
#include <vector> // для std::vector
#include <utility> // для std::pair
bool hasDuplicates(std::vector<std::pair<std::string, int> > pairlist)
{
// здесь какой-то код
return false;
}
int main()
{
std::vector<std::pair<std::string, int> > pairlist;
return 0;
}
Ввод std::vector<std::pair<std::string, int> >
везде, где вам нужно использовать этот тип, может оказаться громоздким. Гораздо проще использовать псевдоним типа:
#include <string> // для std::string
#include <vector> // для std::vector
#include <utility> // для std::pair
// делаем pairlist_t псевдонимом для этого сумасшедшего типа
using pairlist_t = std::vector<std::pair<std::string, int> >;
bool hasDuplicates(pairlist_t pairlist) // используем pairlist_t в параметре функции
{
// здесь какой-то код
return false;
}
int main()
{
pairlist_t pairlist; // создаем экземпляр переменной pairlist_t
return 0;
}
Намного лучше! Теперь нам нужно вводить только pairlist_t
вместо std::vector<std::pair<std::string, int> >
.
Не волнуйтесь, если вы еще не знаете, что такое std::vector
, std::pair
или все эти сумасшедшие угловые скобки. Единственное, что вам действительно нужно понять, это то, что псевдонимы типов позволяют вам брать сложные типы и давать им простые имена, что упрощает работу с этими типами и их понимание.
Вероятно, это лучшее использование псевдонимов типов.
Использование псевдонимов типов для повышения читабельности
Псевдонимы типов также могут помочь в документации и понимании кода.
Что касается переменных, у нас есть идентификатор переменной, который помогает документировать назначение переменной. Но рассмотрим случай значения, возвращаемого функцией. Типы данных, такие как char
, int
, long
, double
и bool
, хороши для описания того, какой тип возвращает функция, но чаще мы хотим знать, какой цели служит возвращаемое значение.
Например, рассмотрим следующую функцию:
int GradeTest();
Мы видим, что возвращаемое значение является целым числом, но что означает целое число? Буквенная оценка? Количество пропущенных вопросов? Идентификационный номер студента? Код ошибки? Кто знает! Тип возвращаемого значения int
мало что говорит нам. Если нам повезет, где-то существует документация по этой функции, к которой мы можем обратиться. Если нам не повезет, мы должны прочитать код и определить назначение сами.
Теперь давайте создадим эквивалентную версию, используя псевдоним типа:
using testScore_t = int;
testScore_t GradeTest();
Использование типа возвращаемого значения testScore_t
делает очевидным, что функция возвращает тип, представляющий результат теста.
По нашему опыту, создание псевдонима типа только для документирования типа возвращаемого значения отдельной функции не стоит усилий (вместо этого используйте комментарий). Но если вы уже создали typedef
по другим причинам, это может быть приятным дополнительным преимуществом.
Использование псевдонимов типов для упрощения поддержки кода
Псевдонимы типов также позволяют изменять базовый тип объекта без изменения большого количества кода. Например, если вы использовали short
для хранения идентификационного номера студента, но позже решили, что вам нужен long
, вам придется прочесать много кода и заменить short
на long
. Вероятно, будет сложно понять, какой short
используется для хранения номеров ID, а какой – для других целей.
Однако с псевдонимом типа всё, что вам нужно сделать, это изменить studentID_t = short;
на studentID_t = long;
.
Хотя это кажется приятным преимуществом, необходимо соблюдать осторожность при изменении типа, поскольку поведение программы также может измениться. Это особенно верно при изменении типа псевдонима типа на тип из другого семейства типов (например, целочисленный тип на значение с плавающей запятой или наоборот)! Новый тип может иметь проблемы со сравнением или делением целых чисел на числа с плавающей запятой или другие проблемы, которых не было у старого типа. Если вы измените существующий тип на какой-либо другой, ваш код следует тщательно повторно протестировать.
Минусы и заключение
Хотя псевдонимы типов предлагают некоторые преимущества, они также вводят в ваш код еще один идентификатор, который необходимо понимать. Если это не компенсируется какими-либо преимуществами для удобочитаемости или понимания, тогда псевдоним типа приносит больше вреда, чем пользы.
Плохо используемый псевдоним типа может взять знакомый тип (например, std::string
) и скрыть его за пользовательским именем, которое необходимо найти. В некоторых случаях (например, с умными указателями, которые мы рассмотрим в будущей главе) скрытие информации о типе также может быть вредным для понимания того, как этот тип должен работать.
По этой причине псевдонимы типов следует использовать в первую очередь в тех случаях, когда есть явное преимущество для читабельности или поддерживаемости кода. Это в большей степени искусство, чем наука. Псевдонимы типов наиболее полезны, когда их можно использовать во многих местах кода, а не в меньшем количестве мест.
Лучшая практика
Разумно используйте псевдонимы типов, если они дают явное преимущество для читабельности или поддерживаемости кода.
Небольшой тест
Вопрос 1
Возьмем следующий прототип функции:
int printData();
Преобразуйте возвращаемое значение int
в псевдоним типа с именем error_t
. В ответ включите обе инструкции: инструкцию псевдонима типа, и обновленный прототип функции.
Ответ
using error_t = int;
error_t printData();