Я ловлю в далёком отголоске,
Что случится на моём веку.
(«Гамлет», Борис Пастернак)
Признаться, пишу на чистом C я не так уж и часто и за развитием языка уже давно не слежу. Но тут произошло два неожиданных события: С вернул себе звание популярнейшего языка программирования по версии TIOBE и случился анонс первой за долгие годы действительно интересной книги, посвящённой этому языку. Поэтому я провёл несколько вечеров за изучением материалов о C2x — следующей версии C.
Самыми, на мой взгляд, интересными нововведениями я и хочу поделиться с читателями Хабра.
Комитет, стандарт и всё-всё-всё
Уверен, большая часть читателей в курсе, как происходит развитие C, но на всякий случай объясню терминологию и расскажу историю языка в двух словах.
В 1989 году популярнейший язык программирования C перешёл на новую ступень развития — стал американским национальным (ANSI) и международным (ISO) стандартом. Эту версию C негласно назвали C89, или ANSI C — в противовес многочисленным диалектам, существовавшим до этого.
Новая редакция стандарта языка выходит примерно раз в десять лет, на текущий момент существует четыре редакции: оригинальный C89, C99, C11, C18. Год публикации следующей версии неизвестен, поэтому документ в работе называют C2x.
Вносит изменения в стандарт специальная рабочая группа — WG14, в которую входят заинтересованные представители индустрии из разных стран. В англоязычной профильной литературе эту группу часто называют “Committee”, а я для её обозначения буду использовать слово «комитет».
Комитет получает в работу от участников предложения, каждому из которых присваивается обозначение вроде N2353. Предложения обычно включают в себя мотивацию внесения изменений, ссылки на другие документы, конкретные изменения стандарта. Предложения могут иметь несколько редакций, каждая из которых получает уникальное обозначение.
Комитет может, к примеру, проголосовать за обсуждение другой редакции предложения, учитывающей замечания, внесение предложенных изменений в стандарт, отказ во внесении изменений и так далее. У каждого предложения может быть один из следующих статусов: не обсуждалось, отправлено на доработку (и, вероятно, будет принято), принято как есть, отклонено.
Эту статью я разбил на три части. Очерёдность установил по степени вероятности внесения изменений в стандарт. В первой части я перечислю те предложения, которые уже принял комитет. Во второй оказались предложения, которые были восприняты положительно, но возвращены авторам на доработку. В последней же части я собрал самое «вкусное» — слухи о неопубликованных предложениях, обсуждаемых «в кулуарах» членами комитета.
Согласованные комитетом предложения
Функции strdup и strndup
Я, вероятно, покажусь невеждой, если скажу, что не знал об отсутствии этих функций в стандартной библиотеке C. Казалось бы, что может быть очевиднее и проще копирования строк? Но нет, C не такой, это вам не POSIX.
Итак, с опозданием лет на 20 к нам приходят функции strdup и strndup!
#include <string.h>
char *strdup (const char *s);
char *strndup (const char *s, size_t size);
Приятно осознавать, что комитет иногда всё же принимает неизбежное.
Атрибуты
У разработчиков больших компиляторов C есть любимая игра: придумывать собственные расширения к языку в форме атрибутов объявлений и определений. Сам язык, конечно же, специального синтаксиса для таких вещей не предоставляет, поэтому каждый изощряется как может.
Чтобы хоть как-то привести этот балаган к порядку без создания десятков новых ключевых слов, комитет придумал синтаксис-который-будет-управлять-всем. Словом, в следующей версии стандарта принимается синтаксис для указания атрибутов. Пример из предложения:
[[attr1]] struct [[attr2]] S { } [[attr3]] s1 [[attr4]], s2 [[attr5]];
Здесь attr1
относится к s1
и s2
, attr2
относится к определению struct S
, attr3
— к типу struct s1
, attr4
— к идентификатору s1
, attr5
— к идентификатору s2
.
За включение атрибутов в стандарт комитет уже проголосовал, но до публикации обновлённой версии стандарта ещё далеко. Поэтому авторы предложений пользуются новой игрушкой. Некоторые из предложенных атрибутов:
- Атрибут
deprecated
позволяет пометить объявление как устаревшее. При использовании таких объявлений компиляторам предлагается предупреждать программиста. - Атрибутом
fallthrough
можно явно пометить места в ветвлениях switch, где поток исполнения должен попасть в следующую ветвь тоже. - С помощью атрибута
nodiscard
можно явно указать необходимость обработки возвращаемого функцией значения. - Если переменная или функция осознанно не используется, можно пометить её атрибутом
maybe_unused
(а не почти уже идиоматичным(void) unused_var
). - Атрибутом
noreturn
можно пометить функцию, которая не вернётся в место вызова.
Традиционный способ указания параметров функций (K&R)
Устаревший ещё в 1989 году такой способ объявления параметров функции, как «объявление в стиле K&R» (он же — «когда типы после скобочек указываются», он же — «я не понимаю старый код на C»), будет, наконец, сожжён на костре, а я смогу расслабиться и не следить за своими void-ами.
Другими словами, больше нельзя будет делать так:
long maxl (a, b)
long a, b;
{
return a > b ? a: b;
}
Эпоха Просвещения приходит в код на C! Объявления функций будут делать именно то, что приличные люди от них ожидают:
/* объявление функции без аргументов */
int no_args();
/* тоже объявление функции без аргументов */
int no_args(void);
Представление знаковых целых чисел
Бесконечная, казалось бы, история близка к завершению. Комитет смирился с тем, что единорогов и сказочных архитектур не существует, а программисты на C имеют дело с дополнительным кодом (англ. two's complement) для представления знаковых целых чисел.
В текущем виде это уточнение немного упростит стандарт, но в перспективе поможет (!) избавиться от «любимого» программистами неопределённого поведения языка — переполнения знакового целого числа.
Предложения в работе
Если перечисленные выше изменения уже, можно сказать, существуют в нашей реальности, то следующая группа предложений пока находится в разработке. Тем не менее комитет предварительно их одобрил и при должном усердии авторов они точно будут приняты.
Безымянные параметры функций
Я стабильно пишу одну-две пробные программы на C в неделю. И, признаться, мне уже давно надоело указывать имена неиспользованных аргументов main
.
Реализация одного из положительно оценённых комитетом предложений позволит не указывать лишний раз имена параметров в определениях функций:
int main(int, char *[])
{
/* И никакой перхоти! */
return 0;
}
Мелочь, но какая приятная!
Старые новые ключевые слова
После долгого, до-о-олгого переходного периода комитет, наконец, решил больше не придуриваться и принять в язык, эм, «новые» ключевые слова: true
, false
, alignas
, alignof
, bool
, static_assert
и другие. Заголовки вроде <stdbool.h>
можно будет, наконец, почистить.
Включение двоичных файлов в исходный файл
Включение двоичных данных из файлов в исполняемый файл — невероятно полезная для всех игроделов возможность :
const int music[] = {
#embed int "music.wav"
};
Надеюсь, члены комитета понимают, что Хабрасообществу известно место проведения следующего заседания, и эта директива препроцессора будет принята без вопросов.
Прощай, NULL, или nullptr на низком старте
Кажется, на смену проблемному макросу NULL
приходит ключевое слово nullptr
, которое будет эквивалентно выражению ((void*)0)
и при приведении типов должно оставаться типом-указателем. Любое использование NULL
предлагается сопровождать предупреждением компилятора:
/* Вы же никогда не пишете просто NULL? Я вот до сих пор затылок чешу. */
int execl(path, arg1, arg2, (char *) NULL);
/* Но счастье близко */
int execl(path, arg1, arg2, nullptr);
Если пример вам ни о чём не говорит, то загляните в документацию для Linux по адресу man 3 exec
, там будет пояснение.
Реформа обработки ошибок в стандартной библиотеке
Обработка ошибок функций стандартной библиотеки — давняя проблема C. Сочетание неудачных решений в ранних версиях стандарта, консервативности комитета и вопросов обратной совместимости не позволяло найти устраивающее всех решение.
И вот наконец нашёлся герой, готовый предложить решение одновременно разработчикам компиляторов, сверхконсервативному комитету и нам, простым смертным:
[[ oob_return_errno ]] int myabs (int x) {
if(x == INT_MIN ) {
oob_return_errno ( ERANGE , INT_MIN ) ;
}
return (x < 0) ? -x : x;
}
Обратите внимание на атрибут oob_return_errno
. Он означает, что из этой функции-шаблона будут сгенерированы следующие функции:
- Возвращающая структуру с флагом ошибки и результатом работы функции (
struct {T return_value; int exception_code}
). - Возвращающая результат работы функции и игнорирующая возможные ошибки в аргументах, приводя к неопределённому поведению.
- Завершающая выполнение в случае ошибки в аргументах.
- Заменяющая
errno
, то есть обладающая привычным поведением.
Компилятору предлагается выбирать между этими вариантами в зависимости от того, как использует функцию программист:
bool flag;
int result = oob_capture(&flag , myabs , input) ;
if (flag) {
abort ();
Здесь корректность выполнения функции показывается флагом flag
, причём errno
не затрагивается. Аналогично выглядят, например, вызовы функций с сохранением кода ошибки в переменную.
Конкретный синтаксис, похоже, ещё будет меняться, но хорошо то, что комитет хотя бы думает в этом направлении.
Слухи
Автор «Effective C» совместно с другими членами комитета отвечали на вопросы участников англоязычного сообщества Hacker News. Вопросов и ответов по ссылке много, многое пересекается с отмеченными выше вещами. Но есть пара важных для программистов пунктов, которые не оформлены как предложения, но члены комитета намекают на то, что какая-то работа в этих направлениях уже ведётся.
Оператор typeof
Ключевое слово typeof
уже давно реализовано в компиляторах и позволяет не повторяться при написании кода. Канонический пример:
#define max(a,b) \
({ typeof (a) _a = (a); \
typeof (b) _b = (b); \
_a > _b ? _a : _b; })
Мартин Себор (Martin Sebor), ведущий разработчик из Red Hat и участник Комитета, утверждает, что соответствующее предложение уже находится в работе и почти наверняка будет одобрено.
Держу пальцы скрещенными.
Оператор defer
Некоторые языки программирования, в том числе и реализованные на базе Clang и GCC, позволяют привязывать высвобождение ресурсов к лексическим областям видимости переменных или, проще говоря, вызывать какой-то код с выходом программы из области видимости переменной.
В чистом C нет и никогда не было такой возможности, но компиляторы уже давно реализуют атрибут cleanup(<cleanup function>)
:
int main(void)
{
__attribute__((cleanup(my_cleanup_function))) char *s = malloc(sizeof(*s));
return 0;
}
Роберт Сикорд (англ. Robert Seacord), автор «Effective C» и член комитета, признался, что работает над предложением в стиле ключевого слова defer
из Go:
int do_something(void) {
FILE *file1, *file2;
object_t *obj;
file1 = fopen("a_file", "w");
if (file1 == NULL) {
return -1;
}
defer(fclose, file1);
file2 = fopen("another_file", "w");
if (file2 == NULL) {
return -1;
}
defer(fclose, file2);
/* ... */
return 0;
}
В приведённом примере функция fclose будет вызвана с аргументами file1
и file2
при любом выходе из тела функции do_something
.
Близится революция!
Выводы
Изменения в C — как мутации в генетике: происходят редко, частенько бывают нежизнеспособны, но в итоге способствуют эволюции.
Последние неудачные изменения в C случились десять лет назад. А последний качественный скачок в разработке на языке произошёл более 20 лет назад. И, судя по всему, в новой итерации стандарта члены комитета решили всё-таки подумать над поступательным движением вперёд.
В общем, пользуйтесь статическими анализаторами, почаще запускайте Valgrind и старайтесь не писать слишком больших программ на C!
PS Пожалуй, я немного загнул насчет "единственной действительно интересной книги". Пользователь mikeus подсказал другую книгу, и тоже от члена комитета, и она тоже стоит того однозначно: Modern C.