Я ловлю в далёком отголоске,
Что случится на моём веку.
(«Гамлет», Борис Пастернак)
Признаться, пишу на чистом 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.
KanuTaH
Ну так-то единороги все ещё существуют:
https://www.unisys.com/offerings/clearpath-forward/clearpath-forward-products/clearpath-forward-dorado-systems
Но скорее уже конечно для запуска business critical софта, разработанного для уже вымерших единорогов, которых в свое время было достаточно.
VlK Автор
забавно, что в документах комитета мейнфреймы даже не упоминались. на Си для них никто не пишет..?
KanuTaH
Да я думаю вполне себе пишут. Вот тут:
unite.org/wp/wp-content/uploads/2014/08/OS3035.pdf
в списке языков для разработки на Dorado наряду с COBOL, FORTRAN, MASM и C есть даже Java и PHP.
Просто видимо считается что им на их век и более старых стандартов C хватит.
VlK Автор
Возможно.
Опять же, за редкие платформы обычно ратуют представленные в комитете разработчики компиляторов, которым это дело надо как-то реализовывать. Если им все равно, то и комитету все равно.
SpiderEkb
IBM PowerS на платформе IBM i (бывш. AS/400) — мейнфрейм?
Там волне себе пишут и на С и на С++. Они входят в состав ILE (Integrated Language Environment — туда входят C/C++, CL, COBOL, RPG) и поддерживаются на уровне самой ОС (т.е. все библиотеки и компилятор входят в состав ОС) — компиляция производится командой CRTBNDC (команда системного языка CL). Для С++ — CRTBNDCPP.
Поддерживается там С11. Ну плюс некоторые специфические для платформы расширения типа поддержки типов с фиксированной десятичной точкой, поддержка специфических менеджеров памяти типа QuickPool, поддержка работы со специфическими типами объектов типа *SYSPTR… Расширение для работы со структурированными файлами (таблицы, индексы, дисплейные и принтерные файлы — RECIO).
Прелесть ILE в том, что можно написать несколько модулей (аналог объектного файла) на разных языках, а потом собрать их в единый программный объект — любую функцию или процедуру можно писать на том языке, на котором ее реализация будет наиболее эффективной.
VlK Автор
Ну, такой Си это не вполне и Си даже, пожалуй :-) по крайней мере комитет их в расчёт не особо берет.
SpiderEkb
Почему? С11 там поддерживается в полном объеме. Плюс расширения, характерные для платформы (платформа очень специфическая во всех отношениях).
Честно говоря, из того что в статье написано, я как-то не очень все хочу. С всегда был языком с понятным кодом — что написал, то и получил. Да, он заставлял держать в голове многое из того, что молодежь держать в голове не привыкла.
Что предлагается сейчас? Ставишь атрибут и получаешь на выходе кучу кода, который генерит компилятор за тебя. Все в угоду тем, кому слишком сложно думать о последствиях того, что они пишут. Ну сложно — есть много других, более высокоуровневых языков. Кто заставляет именно на С писать?
Я сейчас вполне свободно пишу на RPG потому что на нем проще писать многие вещи под AS/400. Но при этом всегда могу то, что требует эффективности и прозрачности С, написать на С. Та же работа с памятью, динамические списки и проч. Все то, чего мне нехватает в RPG, или реализуется там коряво, я просто напишу на С и вызову из RPGшной программы Сшную функцию (не говоря уже о том, что из RPG можно напрямую вызывать функции C RTL просто правильно прописав прототип). Да, при этом нужно понимать что такое передача параметра по ссылке или по значению, что такое манглинг и в каком порядке параметры функции передаются через стек. Может кому-то это сложно… Может кому-то само понятие указателя в голове не укладывается. Но зачем опускать С до уровня школьников? Ради дешевой популярности?
Плюсы изгадили, теперь до чистого С добраться решили?
VlK Автор
Точно. Стандартный Си на мейнфрейме — нечто инородное, и оттуда растут всякие действительно нестандартные расширения.
А что из предлагаемых изменений вдруг приведет к непонятности или непредсказуемости? Все изменения в том или ином виде сто лет в обед как присутствуют в основных компиляторах.
Сомневаюсь, что ваш (или любой приличный в принципе) компилятор советуется с программистами в вопросах генерируемого кода. Времена однопроходных компиляторов закончились где-то в районе позднеперестроечного Союза, уже в начале 90-х появизилсь большие оптимизирующие монстры.
Наличие же атрибутов в синтаксисе современных диалектов Си — факт жизни, они есть во всех взрослых компиляторах.
Жизнь. POSIX и иже с ним определены и написаны в терминах Си. Си и *никс вообще разделить трудно, их разрабатывали совместно, и параллельно стандартизировали. Поэтому если вы работаете с этими ОС, то от Си никуда не деться.
Ага, кошмар! Отморозки удумали исправить — супераккуратно! — обработку ошибок. Да и каких нелюдям захотелось упростить высвобождение ресурсов?! goto это так удобно!
PS я понимаю вашу точку зрения, и понимаю консервативную идеологию языка. Но как реально имеющий дело с языком программист некоторые изменения не могу не приветствовать.
SpiderEkb
Ну, вообще-то AS/400 (точнее, OS/400, ныне называемая i5/OS) есть объектно-оринтированная ОС, написанная на С++ практически полностью (если почитать книгу одного из ее отцов-основателей Френка Солтиса «Основы AS/400»). Основная специфика платформы — концепция «все есть объект». Там нет файлов в привычном для Win и *nix понимании — там есть «объекты», характеризуемые именем и типом, каждый объект обладает своим набором свойств и с любым объектом можно производить только те действия, которые определены для него системой. Ну, к примеру, вы не можете взять, к примеру, программный объект и в HEX редакторе поправить в нем байтики — свойство редактирования для этого объекта системой не поддерживается.
Так что С и С++ там вполне себе как родные. Просто есть набор специфических расширений, типов и прагм сверх стандарта.
Больше скажу, я отдельные алгоритмы, не привязанные к платформе, вообще пишу и отлаживаю под Win на miGW в режиме C11. А потом переношу на ASку (там чтобы отладиться, надо сначала на локалке написать, потом забросить на сервер, там собрать и дебажить в эмуляторе терминала IBM5250, да и дома доступ до сервера AS появился только с переходом на удаленку, есть правда, публичный PUB400.COM, но там совсем неудобно работать — тот же gradle, который у нас настроен на наш сервер, туда не заточить).
А нестандартность расширений… Ну считайте это дополнительными библиотеками. Типа RECIO, позволяющей работать с физическими (таблицы), логическими (индексы) файлами. По идеологии что-то типа Paradox Engine (если кто помнит) для борландовской БД Paradox времен доса и вин 3.х.
Я не про оптимизацию. Я вот про такое:
Или вот такого:
На ASке, с ее концепцией групп активации (Activation Group) как некоторого контейнера внутри задания (job) это может вести себя очень странно.
Суть в том, что если ваша программ работает в своей группе активации, то когда вы ее вызовете несколько раз в рамках одного задания, значения статических и глобальных переменных сохраняются между вызовами. Понимание этого механизма позволяет существенно повышать эффективность за счет выноса всего чего можно в блок инициализации, который будет отрабатывать только при первом запуске программы в рамках задания.
Это относится к чтению настроечных *dtaara (достаточно «тяжелая» операция), подготовке динамических параметризированных sql запросов (операции declare/prepare) с тем, что потом их просто открываешь с параметрами (open… using ....) не тратя каждый раз ресурсы на построение и т.п.
И вот такого:
Ну хочется вам чтобы было как в Go — может проще писать сразу на Go?
Мне вот не приходит в голову работать с типами с фиксированной точкой (packed, zoned) в С/С++. Хотя формально их поддержка есть, но неудобно. Все это прекрасно делается в RPG (зачем они вообще нужны? для коммерческих рассчетов — см. рекуррентное соотношение Мюллера).
Я просто наелся уже всех этих умностей со стороны компиляторов. На прошлом месте занимался разработкой распределенных систем с гарантированным временем отклика (на С++, а вообще на С начал писать в концу 80-х). И напрарывался на то, что когда начинаешь использовать stl со всеми ее шаблонами, программа в критических местах начинает тормозить (ну в нашем тогда понимании). Пришлось всю эту лабуду выкинуть и базовые алгоритмы прописать на чистом С руками. Именно потому, что там знаешь что оно сделает так, как написал. Особенно, если еще оптимизацию отключить (тоже напарывался что критичный по скорости код лучше не оптимизировать компилятором).
Так что моя позиция в том, что с плюсами делайте что хотите, а чистый С оставьте таким какой он есть. С минимальными изменениями в плане синтаксиса (типа добавить bool и подобные вещи).