Два способа использования одноэлементных структур в Cи с пользой

В структурах языка Си очень много странностей, но, по большей части, они предсказуемы, полезны и понятны.

Для тех, кто не знаком с Cи: структуры представляют собой наборы данных. Примером их использования является точка на декартовой плоскости:

struct point {
    int x;
    int y;
};

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

Массив с защищенным типом информации

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

Рассмотрим несколько примеров использования оператора sizeof в языке Си. Во-первых, познакомимся с обычным поведением массивов. В этом примере мы выделим память под массив и будем ссылаться на него по имени, под которым он был объявлен.

void direct_reference() {
    uint8_t example_array[10];
    printf("%lu\n", sizeof(example_array));
} 
  
int main() {
    direct_reference();
    return 0;
}

Этот пример при запуске выводит на консоль, как и ожидалось, значение «10». Если же заменить функцию direct_reference новой функцией под названием indirect_reference, то результат компиляции будет совершенно другим.

void indirect_reference(uint8_t referenced_array[10]) {  
    printf("%lu\n", sizeof(referenced_array));
} 

int main() {  
    uint8_t example_array[10];  
    indirect_reference(example_array);     
    return 0;
}

На 64-разрядной машине этот пример выводит значение «8», что является, вероятно, размером указателя в системе. Обратите внимание, что sizeof рассматривает referenced_array как указатель, хотя мы явно определили его тип.

К счастью, мы можем использовать структуру с одним элементом, чтобы сохранить информацию о размере массива, несмотря на ограничения при ссылке! Например:

struct array_wrapper {  
    uint8_t array[10];
}; 

void indirect_reference(struct array_wrapper * a) {  
    printf("%lu\n", sizeof(a->array));
} 

int main() {  
    struct array_wrapper ar;  
    indirect_reference(&ar);     
    return 0;
}

В этом примере, мы определяем структуру с одним полем — массивом. Интересно, что если мы будем в любой части кода использовать ссылку на структуру и применим оператор sizeof к массиву внутри конструкции, его размер не изменится. Структура сохраняет всю информацию о размере своих элементов. Выполнение этого кода, как и ожидалось, выводит значение «10».

То же самое будет происходить и при использовании внешних ссылок (при использовании extern).

Примечание: Тот же самый эффект сохранения информации о размере может быть достигнут при использовании оператора typedef вместо структуры.

Предотвращение нежелательного приведения типов

Язык Cи обеспечивает механизм, с помощью которого можно переименовать типы. Ключевое слово typedef дает нам возможность создавать такие типы, как count_t, который на самом деле является типом int32_t. Это хорошо для семантики, но не дает дополнительной надежности во время компиляции.

И ничто не помешает вам, скажем, определить тип с именем seconds и другой тип с именем milliseconds, а затем случайно сложить их! В обоих случаях Си воспримет оба типа как целочисленные и выполнит операцию сложения! Это, конечно же, приведет к бессмысленному результату. Если добавить 5 миллисекунд к 10 секундам, вы, в конечном счете, получите 15 секунд или миллисекунд, что, естественно, не является правильным и желаемым выполнением кода.

Как вы уже догадались, мы можем использовать одноэлементную структуру, чтобы добиться дополнительной надежности при компиляции, чего typedef сам по себе не обеспечивает! В следующем примере мы видим ситуацию, когда миллисекунды и секунды ошибочно суммируются.

typedef uint32_t seconds_t; 
typedef uint32_t milliseconds_t; 

int main() {  
    seconds_t x = 10; 
    milliseconds_t y = 20;  
    // oops!  
    seconds_t result = x + y; 
    printf("seconds: %u\n", result); 
}

Мы можем добиться немного более надежного выполнения, сделав следующее. Во-первых, вместо того чтобы использовать typedef, мы можем указать значения в структуре! Во-вторых, мы можем определить функцию, которая выполняет сложение секунд. Далее представлен более полный пример.

struct seconds { uint32_t val; }; 
struct milliseconds { uint32_t val; };  

struct seconds add_seconds(struct seconds a, struct seconds b) 
 { return (struct seconds) { a.val + b.val }; } 

int main() {
    struct seconds x = { 10 };
    struct milliseconds y = { 20 };
    // oops!
    struct seconds result = add_seconds(x, y);
    printf("seconds: %u\n", result.val);
}

В этом примере мы передаем целочисленные значения, тип которых определен в структурах, функции, осуществляющей суммирование чисел. Вы заметите, что этот пример не компилируется! Компилятор может выдать следующую ошибку:

error: passing ‘struct milliseconds’ to parameter of incompatible type ‘struct seconds’
struct seconds result = add_seconds(x, y);

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

Перевод статьи «2 Ways to Use Single-Member structs in C»