[Перевод] Почему язык C никогда не помешает вам совершать ошибки
Короткий ответ: потому что мы так сказали.
:)
… Что?
Ладно, признаю, для статьи это неприемлемо короткий ответ, и мои провокационные слова требуют пояснений.
Изначально проведение конференции Комитета по С было запланировано во Фрайбурге (Германия). Но по некоторой причине она состоялась не там и завершилась в пятницу, 7 августа. Встреча прошла хорошо, мы добились значимого прогресса по всем направлениям. Мы делаем реальные успехи — я вас уверяю, что действительно делаем, и C — не мертвый язык.
Попутно замечу, что я также стал редактором проекта для C. Поэтому прежде, чем вы воспримете этот текст как тираду неосведомленного человека, слишком ленивого, чтобы заниматься «улучшением ситуации», позвольте мне вас заверить, что я действительно заинтересован в том, чтобы C начал удовлетворять потребности разработчиков, при этом не нуждаясь в 50 специфических расширениях для создания хоть сколько-нибудь удаленных хороших/полезных библиотек и приложений.
Тем не менее, я сделал утверждение, и я должен его обосновать. Мы могли бы рассмотреть тысячи CVE — список известных уязвимостей и дефектов безопасности) и проблем, свойственных коду на языке С, или необходимость для MISRA тщательно контролировать каждую маленькую потенциальную фичу языка С, чтобы предотвратить неправильное использование (привет, объявления прототипов по K&R…), или другие более сложные и забавные баги, связанные с переносимостью и неопределенным поведением. Но вместо этого мы просто рассмотрим информацию из уст самого Комитета.
О, время доставать попкорн?!
Нет, дорогой читатель, не торопись доставать попкорн. Как и во всех процедурах ISO, мне нельзя цитировать кого-либо дословно. И это не статья, в которой мы будем называть конкретные имена и заниматься шеймингом. Однако в ней мы постараемся объяснить, почему случаи, которые мы можем легко диагностировать и идентифицировать как плохое поведение, в стандартном С никогда не исчезнут. И начнем мы с документа доктора Филиппа Клауса Краузе (Dr. Philipp Klaus Krause): «N2526, использование const для неизменяемых данных из стандартной библиотеки».
N2526 — это очень простой документ. «Некоторые данные, возвращаемые библиотекой, являются const
морально, духовно и фактически даже по своей реализации. Это неопределенное поведение и писать в них — неправильно, так что давайте перестанем издеваться друг над другом по этому поводу». … Ладно, там не совсем так написано, но я уверен, дорогой читатель, что идею вы поняли. Изначально, когда проводилось голосование за этот документ, голосов против почти не было. Затем несколько человек решительно выразили возражения, потому что он ломает старый код. Конечно, это плохо. Даже у меня перехватило дыхание, и я с напряжением подумал — добавить const
? У C нет ABI, на который это могло бы повлиять, C (его реализации) даже не учитывает квалификаторы, как мы можем что-то сломать?! Итак, давайте поговорим о том, почему в глазах некоторых людей это стало бы критическим изменением.
Язык С
Или, как мне нравится его называть, «язык «Типобезопасность — это для неудачников». Конечно, это слишком громко сказано, поэтому придется обойтись просто «С». Возможно, вам интересно, почему я утверждаю, что у языка С нет типобезопасности. Если на то пошло,
struct Meow {
int a;
};
struct Bark {
double b;
void* c;
};
int main (int argc, char* argv[]) {
(void)argc;
(void)argv;
struct Meow cat;
struct Bark dog = cat;
// error: initializing 'struct Bark' with an expression of incompatible type 'struct Meow'
return 0;
}
Давайте честно: по мне, Джим, это похоже на строгую типизацию! Дальше, конечно, все становится только пикантнее:
#include
struct Meow {
int a;
};
struct Bark {
double b;
void* c;
};
int main (int argc, char* argv[]) {
(void)argc;
(void)argv;
struct Meow* p_cat = (struct Meow*)malloc(sizeof(struct Meow));
struct Bark* p_dog = p_cat;
// :3
return 0;
}
и̷͎̭̫͐̕б̸̮́͜о̷̝́̈́̀͜ ̵̺̳͍̀́̀я̴͇̳̊̉̑ ̵̜͍̂̒п̸̬̻̱̈́͋̀ё̶͍̄̓с̵̜̞͊-̶̳̗̊р̵̟̮̫͐͛̎а̶̘͈̎̍̚з̸͓͆͋͊р̷̳͉̘̇̃͝у̸͚͔̬̃͌͆ш̷͖̰͕̎ӥ̴͎́̇̊т̸̜͌̇е̷̢͕̞̃л̵̲̑ь̴̧͒̚ ̴̜̌̈́ͅв̶̱͔͓̐с̵̝͇̎̚ё̶̠̱̞̈́г̵̮̓͋͝о̵͖̳͌͂»
Да, два совершенно несвязанных типа указателей могут быть могут быть приравнены друг к другу в следующем стандарту C. Большинство компиляторов предупреждают об этом, но по стандарту этот код будет принят, если только вы не зададите опции -Werror
-Wall
-Wpedantic
и т.д. и т.п.
Также без явного преобразования компилятор может принять следующие вещи, связанные с указателями :
volatile
(кому вообще нужна эта семантика?!)const
(записывайте в любые данные, доступные только для чтения!)_Atomic
(потоковая безопасность, шмотоковая безопасность!)
Заметьте, я не утверждаю, что у вас вообще не должно быть возможности делать эти вещи. При работе с языком С легко получить функцию в 500 или 1000 строк с именами переменных, которые не описывают, для чего они предназначены. И Неопровержимый Факт™ заключается в том, что вы работаете в основном с указателями, и у вас нет никакой безопасности в той мере, в какой это касается основ языка. (Примечание: Это действительно нарушение ограничений, но есть так много legacy-кода, что каждая реализация игнорирует квалификаторы, так что код никогда не перестанет компилироваться из-за этого (спасибо, @fanf!)! Каждый потенциальный сбой здесь можно легко диагностировать с помощью компилятора, и все из них выдают предупреждения, но никогда не потребуют от вас преобразования типа, чтобы сообщить компилятору, что вы действительно имели это в виду. Что гораздо важнее, это также означает, что люди, которые придут после вас, не будут иметь ни малейшего представления о том, действительно ли вы имели это в виду.
Достаточно убрать из сборки -Werror
-Wall
-Wpedantic
, и вы сможете совершать преступления, связанные с многопоточностью, доступом только для чтения и аппаратными регистрами.
Это ведь справедливо, правда? Если кто-то убирает все эти флаги предупреждения/ошибки, то ему, очевидно, нет дела до того, какую бестактность или глупую оплошность вы совершили. Это означает, что в конечном итоге эти предупреждения не имеют никакого значения и безвредны с точки зрения соответствия стандартам ISO C. И все же…
Мы считаем изменения в предупреждениях нарушением совместимости
Да.
Это отдельный вид ада, с которым свыклись разработчики языка С, и в меньшей степени разработчики C++. Предупреждения воспринимаются как раздражители; и, как показывает любое включение -Weverything
или /W4
, таковыми они и являются. Предупреждения о затенении переменных в глобальном пространстве имен (спасибо, все заголовки и библиотеки С теперь являются проблемой), использование «зарезервированных» имен, и «для этой структуры включено выравнивание, потому что вы использовали оператор alignof
(… да, я знаю, что для неё включено выравнивание, я явно запросил его включить И ПОЭТОМУ Я ИСПОЛЬЗОВАЛ alignof
, мистер Компилятор) — все это невероятно затратно по времени.
Но это предупреждения.
Несмотря на то, что они раздражают, они помогают предотвратить проблемы. Тот факт, что я могу бездумно игнорировать все квалификаторы и ломать все виды безопасности, связанные с чтением, записью, потоками и неизменяемостью, становится серьезной проблемой, когда речь идет о коммуникации намерений и предотвращении багов. Даже старый синтаксис K&R приводил к ошибкам в промышленных и правительственных кодовых базах, потому что пользователи что-то делали неправильно. Это не потому, что они плохие программисты: это потому, что они работают с кодовыми базами, которые бывают старше их самих, и вынуждены иметь дело с техническим долгом на много миллионов строк кода. Не получится держать всю кодовую базу в голове: именно с этим должны бороться конвенции, статический анализ, высокие уровни предупреждений и все остальное. К сожалению,
всем нравится код без предупреждений.
Это означает, что в тот момент, когда разработчик GCC сделает предупреждение более чувствительным к потенциальным проблемным случаям, люди, поддерживающие кодовую базу (не изначальные разработчики) внезапно получат логи на несколько гигабайт, содержащие тонну предупреждений и прочих штук из их старого кода. «Это глупо», — скажут они, — «код работает уже ГОДАМИ, почему GCC начал жаловаться только сейчас?». Это означает, что избегаются даже такие действия, как добавление const
в сигнатуру функции, даже если это с моральной, духовной и фактической стороны правильно. Нарушение совместимости означает, что людям теперь придется смотреть в код, который имеет сомнительные намерения. Этот код из-за неопределенного поведения выведет из строя чип или испортит память, но это уже другая проблема разработки на C в современной экосистеме.
Возраст как мера качества
Сколько вообще людей догадались бы, что в sudo
есть уязвимость, столь же фантастически простая, как »-1 или целочисленное переполнение дает вам доступ ко всему»? Сколько людей думали о том, что Heartbleed станет реальной проблемой? Сколько разработчиков игр используют «крошечные» библиотеки stb, ни разу не запустив на них фаззинг и не поняв, что они содержат более значительные уязвимости ввода, чем они могли себе представить? Это не упрек в адрес ввышеперечисленных фрагментов кода или программистов, стоящих за ними: они предоставляют жизненно важную услугу, от которой мир зависел на протяжении десятилетий, часто практически без поддержки, пока все это не превратилось в большую проблему. Но люди, которые этому коду поклоняются и деплоят его, в конечном итоге поддерживают токсичную идею, появившуюся под влиянием ошибки выжившего:
«Этому коду так много лет, его использовали столько человек — как у него могут быть проблемы?».
Придерживаясь высших идеалов разработки кода на языке С — принципа обратной совместимости и стремления не становиться «источником неудобств, люди с приличным опытом в индустрии начинают приравнивать возраст к качеству, как если бы кодовые базы были бочками вина в погребе. Чем старше и дольше используется код, тем лучше и вкуснее вино.
Реальность, к сожалению, гораздо менее романтична: полный багов и уязвимостей, с каждым днем техдолг растет и становится все более опасным. Каждая система превращается в полуживую, непроработанную и частично необслуживаемую гниющую шелуху. Их приукрашивают с целью придать им благородный вид, но на самом деле это забальзамированный труп, который только и ждет, чтобы его ткнули пальцем не туда, и тогда его гнойные застарелые нарывы лопнут и зальют приложение своим годами выдержанным ботулизмом.
Окей… Мерзко, но как насчет стандарта C?
Проблема, которую я заметил за свое короткое пребывание в качестве члена комитета, заключается в том, что мы предпочитаем обратную совместимость всему остальному, делаем выбор в сторону старых приложений и их юзкейсов, а не какого-либо шанса улучшения надёжности, безопасности и читабельности кода для тех кто переходит на С, даже сегодня. Документ доктора Краузе настолько мал, что практически не вызывает споров: если кому-то не нравятся предупреждения, он может их отключить. Они являются предупреждениями, а не ошибками, не просто так: абстрактная машина языка С не требует диагностики, ISO C позволяет принимать этот код в самых строгих режимах сборки, а это предложение помогло бы остальному миру отучиться от API, в которых четко сказано: «Изменение содержимого того, что вам вернули, является неопределенным поведением».
И тем не менее, мы пересмотрели свое мнение о документе после того, как в качестве причины было приведено «мы не можем вводить новые предупреждения».
Аргументом против было, по сути, «если мы изменим эти сигнатуры, много кода сломается». Это, опять же, представляет изменение предупреждений как нарушение совместимости в поведении (помните, неявные преобразования, удаляющие квалификаторы — даже _Atomic
— вполне допустимы в ISO C, даже если это нарушение ограничений). Если бы это было так, каждый разработчик компилятора должен был бы ввести нечто подобное Rust epoch, но только для предупреждений, чтобы дать людям «стабильный» набор, с которым всегда можно проверять код. Это не совсем новое мнение — я читал статьи некоторых инженеров Coverity о том, как они работают над созданием новых предупреждений и о реакции клиентов на них. Управление «доверием разработчиков» к новым предупреждениям и другим вещам — сложная работа. Требуется много времени, чтобы убедить разработчиков в их полезности. Даже Джону Кармаку потребовалось время, чтобы получить от инструментов статического анализа надлежащий набор предупреждений и ошибок, подходящих для его разработки, прежде чем он пришел к выводу, что «не использовать статистический анализ — безответственно».
И все же, как комитет, мы боремся с добавлением const
к 4 возвращаемым значениям стандартных функций, потому что это добавит предупреждения к потенциально опасному коду. Мы возражали против признания устаревшим старого синтаксиса K&R, несмотря на видимые свидетельства как ошибок по незнанию, так и видимые уязвимости из-за того, что разработчики передают неправильные типы. Мы чуть не добавили неопределенное поведение в препроцессор, только для того, заставить одну особенную реализацию C «сделать все правильно». Мы всегда балансируем на грани того, чтобы сделать объективно неправильную вещь по причинам обратной совместимости. И это, дорогой читатель, в будущем С меня пугает больше всего.
Стандарт C вас не защищает
Не заблуждайтесь — что бы ни говорили вам программисты, поведение руководящего органа C совершенно ясно. Мы не будем вводить предупреждения в ваш старый код, даже если этот старый код может быть опасен. Мы не будем уводить вас от ошибок, потому что это может поколебать фасад, за которым станет видно, что то, что делает старый код, на самом деле неправильно. Мы не будем облегчать новым программистам написание лучшего кода на С. Мы не будем требовать, чтобы старый код соответствовал какому-либо стандарту. Каждую новую фичу мы сделаем необязательной, потому что мы не представляем себе, чтобы разработчики компиляторов придерживались более высоких стандартов, и не ожидаем большего от поставщиков стандартной библиотеки.
Мы позволим компилятору лгать вам. Мы будем лгать вашему коду. А когда что-то пойдет не так — ошибка, «опаньки», утечка данных — мы печально покачаем головой. Мы выразим свою поддержку и добавим: «Как жаль». Действительно, жаль…
Может быть, мы исправим это в другой раз, дорогой читатель.
А в заключение статьи приглашаем всех желающих разработчиков на C на открытый урок, посвященный стандарту С23. На нем рассмотрим:
устаревшие и удалённые возможности языка,
новые языковые конструкции,
изменения в стандартной библиотеке.
Записаться на открытый урок можно на странице курса «Программист С».