Реклама
Перетяжка // Коробка 3.0

Void в C: четыре значения ключевого слова

Кажется, void — это просто «пустота». На деле в C и C++ оно играет четыре разные роли: от функций без возвращаемого значения до указателей на что угодно. Разбираем историю слова, предложенного создателем Bourne Shell, и ловушки, в которые до сих пор попадают новички.

Обложка: Void в C: четыре значения ключевого слова

В C всего 32 ключевых слова, и void кажется одним из самых простых: оно обозначает «ничего». Но если присмотреться, это «ничего» играет в языке как минимум четыре разные роли. Более того, именно void, а не печально известный static, можно считать самым «перегруженным» ключевым словом в C: у static три смысла, а у void — четыре.

Разбираем, откуда взялось это слово — его придумал создатель Bourne Shell Стивен Борн — и зачем оно нужно в современном C и C++.

Ключевые выводы

— В C и C++ ключевое слово void используется четырьмя способами: возвращаемый тип, отсутствие параметров, указатель на неизвестный тип и явное приведение к «ничему».

— Слово void предложил Стивен Борн, автор Bourne Shell; до этого функция без возвращаемого значения просто не указывала тип.

— f(void) и f() — разные вещи в C: первая форма явно говорит «параметров нет».

— void* — универсальный указатель, но разыменовывать его нельзя.

— Приведение к void помогает подавить предупреждения о неиспользуемом возвращаемом значении или параметре.

Краткая история: как появилось void

В самой ранней версии C, так называемом K&R-диалекте, функция без возвращаемого значения писалась без типа. Если возвращаемый тип не указан, компилятор считал, что функция возвращает int. Параметры тоже можно было описывать отдельно от списка аргументов — прямо как в старых учебниках:

			print_locus(file, line, col, msg)
    char *file, *msg;
{
    printf("%s:%d,%d: %s", file, line, col, msg);
}
		

Такой код сегодня выглядит экзотично, но именно так писали до появления прототипов функций. При этом компилятор всё равно генерировал машинную команду возврата и передавал вызывающей стороне какое-то мусорное значение. Хотя вызывающий не ждал результата, это было неэффективно и неочевидно.

Это раздражало Стивена Борна — да, того самого, который создал Bourne Shell. Он пожаловался Деннису Ритчи, и Ритчи увидел выгоду: если явно сказать, что функция ничего не возвращает, можно сэкономить одну машинную инструкцию. Так в C появилось слово void.

Позже void обросло новыми смыслами и перекочевало в C++, C#, Java, D и Swift — хотя Swift предпочитает писать его с заглавной буквы: Void.

Четыре смысла void

1. Функция не возвращает значение

Самый известный смысл. Вместо того чтобы молча возвращать int, функция явно объявляет: «я работаю ради побочного эффекта».

			void print_greeting(const char *name)
{
    printf("Hello, %s!\n", name);
}
		

В современном C и C++ большинство функций без возвращаемого значения пишутся именно так. Это делает намерения автора очевидными и помогает компилятору ловить ошибки: если вы случайно попытаетесь использовать результат void-функции, получите ошибку компиляции.

2. Явное отсутствие параметров: f(void)

В C++ пустые скобки f() означают «функция не принимает параметров». В K&R C те же пустые скобки означали лишь «это функция», без всякой информации о параметрах. Чтобы сохранить совместимость со старым кодом и одновременно ввести прототипы, в C придумали запись f(void):

			/* C: функция не принимает аргументов */
void reset_counter(void);

/* C++: void можно опустить */
void reset_counter();
		

Разница тонкая, но важная. Если в C объявить void foo();, а потом вызвать foo(42), компилятор не проверит количество и типы аргументов — это старая K&R-форма. Запись void foo(void); защищает от таких ошибок.

Совет:
Всегда пишите int main(void) в C. Форма void main() не соответствует стандарту и может ломать работу runtime.

3. Указатель на неизвестный тип: void*

Объект типа void создать нельзя, но указатель на void — можно. void* — это универсальный указатель: он может хранить адрес данных любого типа. В отличие от char*, который в K&R C выполнял ту же роль, void* чётко говорит: «тип здесь не важен».

			int x = 42;
void *p = &x;          /* OK: void* хранит адрес int */

/* Нельзя разыменовать void* напрямую */
/* printf("%d\n", *p); */ /* ошибка */

int *ip = p;            /* в C — неявное приведение; в C++ нужен cast */
printf("%d\n", *ip);
		

Практически все функции динамического выделения памяти — malloc, calloc, realloc — возвращают void*. В C это удобно: компилятор неявно приводит void* к целевому типу. В C++ такой неявный каст запрещён, поэтому там пишут int* arr = (int*)malloc(...) или, что лучше, используют new.

4. Приведение к void

Последний смысл — самый неочевидный. Приведение выражения к void означает: «я намеренно игнорирую результат». Это полезно в двух случаях.

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

			(void)printf("hello, world!\n");
		

Такая запись читается как «вызываем функцию только ради побочного эффекта». Большинство компиляторов и так не ругаются на неиспользованный результат printf, но для функций с атрибутом [[nodiscard]] приведение к void — единственный легальный способ проигнорировать результат.

Второй случай — неиспользуемый параметр функции. В callback-интерфейсах часто приходится принимать аргументы, которые конкретная реализация не использует:

			static bool visitor(c_ast_t const *ast, user_data_t user_data)
{
    (void)user_data;  /* явно говорим: параметр не нужен */
    /* ... работаем с ast ... */
}
		

В C23 можно просто опустить имя параметра, а в C++ — всегда можно было. Но приведение к void работает в любом стандарте и явно показывает намерение.

Практика: что проверить в своём коде

  • В C пишите int main(void), а не void main().
  • Для функций без параметров используйте f(void), особенно в заголовочных файлах.
  • Не разыменовывайте void* — сначала приведите указатель к конкретному типу.
  • Если игнорируете возвращаемое значение, добавьте (void) — это помогает при включённых предупреждениях.
  • Не пытайтесь объявить переменную типа void: объект «ничего» в C не существует.
All uses of void are valid in both C and C++. If that weren't the case, I would have said so.
Paul J. LucasRetired Principal Software Engineer, автор заметки «Into the Void»
Часто задаваемые вопросы
1
Сколько значений у void в C и C++?

Как минимум четыре: возвращаемый тип функции, явное отсутствие параметров в C, универсальный указатель void* и приведение к void для игнорирования значения.

2
Можно ли создать переменную типа void?

Нет. void — неполный тип, объект этого типа не может существовать. Но указатель на void (void*) — вполне реальная и полезная сущность.

3
Чем f(void) отличается от f() в C?

f(void) — современный прототип: функция не принимает аргументов. f() — старая K&R-форма, не несущая информации о параметрах; компилятор не проверит аргументы вызова.

4
Зачем писать (void) перед вызовом функции?

Это сигнал «я намеренно игнорирую возвращаемое значение». Особенно полезно для функций с атрибутом [[nodiscard]] и при строгих настройках компилятора.

5
Почему void main() считается ошибкой?

Стандарт C требует, чтобы main возвращала int. Форма void main() негарантирована: на некоторых системах она может ломать код завершения программы и инициализации runtime.

Выводы

void — не просто синтаксический сахар для «пустоты». Это ключевое слово, которое помогает компилятору различать четыре разных ситуации: отсутствие результата, отсутствие параметров, неизвестный тип данных и намеренное игнорирование значения.

Исторически void появилось из инженерной прагматики: сэкономить одну инструкцию возврата и сделать код понятнее. Сегодня оно остаётся незаменимым инструментом — особенно если вы пишете на C, работаете с системными библиотеками или объясняете студентам, почему void main() — не лучшая идея.

Источник: Paul J. Lucas, «Into the Void», Dev.to. Дополнительный справочник по void — cppreference.com.