Почему язык C никогда не помешает вам совершать ошибки

от автора

Короткий ответ: потому что мы так сказали.

🙂

… Что?

Ладно, признаю, для статьи это неприемлемо короткий ответ, и мои провокационные слова требуют пояснений.

Изначально проведение конференции Комитета по С было запланировано во Фрайбурге (Германия). Но по некоторой причине она состоялась не там и завершилась в пятницу, документа доктора Филиппа Клауса Краузе (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 <stdlib.h>  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 сделает предупреждение более чувствительным к потенциальным проблемным случаям, люди, поддерживающие кодовую базу (не изначальные разработчики) внезапно получат логи на несколько гигабайт, содержащие тонну предупреждений и прочих штук из их старого кода. «Это глупо», — скажут они, — «код работает уже ГОДАМИ, почему испортит память, но это уже другая проблема разработки на C в современной экосистеме. 

Возраст как мера качества

Сколько вообще людей догадались бы, что в sudo есть уязвимость, столь же фантастически простая, как «-1 или целочисленное переполнение дает вам доступ ко всему»? Сколько людей думали о том, что Heartbleed станет реальной проблемой? Сколько разработчиков игр используют «крошечные» библиотеки stb, ни разу не запустив на них фаззинг и не поняв, что они содержат более значительные уязвимости ввода, чем они могли себе представить? Это не упрек в адрес ввышеперечисленных фрагментов кода или программистов, стоящих за ними: они предоставляют жизненно важную услугу, от которой мир зависел на протяжении десятилетий, часто практически без поддержки, пока все это не превратилось в большую проблему. Но люди, которые этому коду поклоняются и деплоят его, в конечном итоге поддерживают токсичную идею, появившуюся под влиянием ошибки выжившего:

«Этому коду так много лет, его использовали столько человек — как у него могут быть проблемы?».

Придерживаясь высших идеалов разработки кода на языке С — принципа обратной совместимости и стремления не становиться «источником неудобств, люди с приличным опытом в индустрии начинают приравнивать возраст к качеству, как если бы кодовые базы были бочками вина в погребе. Чем старше и дольше используется код, тем лучше и вкуснее вино.

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

Окей… Мерзко, но как насчет стандарта C?

Проблема, которую я заметил за свое короткое пребывание в качестве члена не использовать статистический анализ — безответственно».

И все же, как комитет, мы боремся с добавлением const к 4 возвращаемым значениям стандартных функций, потому что это добавит предупреждения к потенциально опасному коду. Мы возражали против признания устаревшим старого синтаксиса K&R, несмотря на видимые свидетельства как ошибок по незнанию, так и видимые уязвимости из-за того, что разработчики передают неправильные типы. Мы чуть не добавили неопределенное поведение в препроцессор, только для того, заставить одну особенную реализацию C «сделать все правильно». Мы всегда балансируем на грани того, чтобы сделать объективно неправильную вещь по причинам обратной совместимости. И это, дорогой читатель, в будущем С меня пугает больше всего.

Стандарт C вас не защищает 

Не заблуждайтесь — что бы ни говорили вам программисты, поведение руководящего органа C совершенно ясно. Мы не будем вводить предупреждения в ваш старый код, даже если этот старый код может быть опасен. Мы не будем уводить вас от ошибок, потому что это может поколебать фасад, за которым станет видно, что то, что делает старый код, на самом деле неправильно. Мы не будем облегчать новым программистам написание лучшего кода на С. Мы не будем требовать, чтобы старый код соответствовал какому-либо стандарту. Каждую новую фичу мы сделаем необязательной, потому что мы не представляем себе, чтобы разработчики компиляторов придерживались более высоких стандартов, и не ожидаем большего от поставщиков стандартной библиотеки.

Мы позволим компилятору лгать вам. Мы будем лгать вашему коду. А когда что-то пойдет не так — ошибка, «опаньки», утечка данных — мы печально покачаем головой. Мы выразим свою поддержку и добавим: «Как жаль». Действительно, жаль…

Может быть, мы исправим это в другой раз, дорогой читатель.

А в заключение статьи приглашаем всех желающих разработчиков на C на открытый урок, посвященный стандарту С23. На нем рассмотрим:

  • устаревшие и удалённые возможности языка,

  • новые языковые конструкции,

  • изменения в стандартной библиотеке.

Записаться на открытый урок можно на странице курса «Программист С».


ссылка на оригинал статьи https://habr.com/ru/companies/otus/articles/747070/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *