C2x: будущий стандарт C

7jdw5r9gajn2olyfuypvvwskuve.png


Я ловлю в далёком отголоске,
Что случится на моём веку.
(«Гамлет», Борис Пастернак)

Признаться, пишу на чистом 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.

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


  1. Атрибут deprecated позволяет пометить объявление как устаревшее. При использовании таких объявлений компиляторам предлагается предупреждать программиста.


  2. Атрибутом fallthrough можно явно пометить места в ветвлениях switch, где поток исполнения должен попасть в следующую ветвь тоже.


  3. С помощью атрибута nodiscard можно явно указать необходимость обработки возвращаемого функцией значения.


  4. Если переменная или функция осознанно не используется, можно пометить её атрибутом maybe_unused (а не почти уже идиоматичным (void) unused_var).


  5. Атрибутом 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. Он означает, что из этой функции-шаблона будут сгенерированы следующие функции:


  1. Возвращающая структуру с флагом ошибки и результатом работы функции (struct {T return_value; int exception_code}).


  2. Возвращающая результат работы функции и игнорирующая возможные ошибки в аргументах, приводя к неопределённому поведению.


  3. Завершающая выполнение в случае ошибки в аргументах.


  4. Заменяющая 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(free))) 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!

© Habrahabr.ru