8.6 – typedef и псевдонимы типов

Добавлено 15 мая 2021 в 22:44
Последнее редактирование 20 июня 2021 в 07:50

Ключевое слово 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();

Теги

C++ / CppLearnCpptypedefДля начинающихОбучениеПрограммированиеПсевдоним типа / Type alias

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.