Как стать автором
Обновить
599.13
OTUS
Цифровые навыки от ведущих экспертов

Линус Торвальдс: Критика C++ — Комплексный анализ

Уровень сложностиПростой
Время на прочтение12 мин
Количество просмотров22K
Автор оригинала: Jan Kammerath

Линус Торвальдс, создатель (и великодушный диктатор) Linux, всегда с особой критикой относился к C++, объясняя почему он отвергает его в разработке ядра Linux. Но он не просто резко высказывается против использования C++, а приводит ряд аргументов, которые мы с вами сегодня и рассмотрим.

В чем причина неприятия C++? Давайте рассмотрим аргументы, которые Линус приводит против его использования.

C и C++ очень похожи друг на друга, но все‑таки это не одно и то же. C++ является объектно‑ориентированной версией C. Некоторые даже называют его преемником C. Однако C++ представляет из себя скорее расширение языка C, нежели его полноценную замену. Он добавляет такие функции, как объекты, конструкторы, деструкторы, шаблоны, обработка исключений, пространства имен и перегрузка операторов. Эти расширения приносят с собой свои собственные парадигмы и проблемы. Неудивительно, что все технические аргументы Линуса против C++ связаны именно с этими расширениями.

Аргументы Линуса против C++

Основные аргументы Линуса против C++ можно найти в рассылке для разработчиков ядра в разделе «Compiling C++ kernel module + Makefile», который датируется 2004 годом. Давайте отложим в сторону личные споры из этих сообщений и сосредоточимся на сути аргументов.

Обработка исключений в C++

«Вся обработка исключений в C++ фундаментально сломана, особенно в контексте ядер».

В отличие от языка C, который полагается на возвращаемые значения для индикации ошибок, C++ поощряет использование исключений. Однако, исключения могут возникать в любой части кода, что делает код, генерирующий их, в некоторой степени непредсказуемым. Они обладают недетерминированным характером и придают C++ совершенно иную парадигму обработки ошибок. Обработка ошибок является неотъемлемой частью программирования, и подход C++ к этому аспекту фактически делает его совершенно другим языком программирования.

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

Для какого‑нибудь приложения для рисования в GNOME это не представляет особой проблемы, но для ядра Linux с его 30 миллионами строк кода это является реальным риском и угрозой. Внедрение обработки исключений в ядро Linux неизбежно сделает его более нестабильным. Хотя тестирование и утверждение изменений в ядре очень строгие, полностью избежать всех ошибок невозможно.

Управление памятью в компиляторах C++

«Любой компилятор или язык, который норовит делать такие вещи, как выделение памяти, за вашей спиной, просто не может быть хорошим выбором для ядра».

Обработка исключений действительно влечет за собой накладные расходы, которые обычно скрыты в компиляторе. Этот аргумент о «скрытом управлении памятью» относится к RAII (Resource Acquisition Is Initialization), или автоматическому управлению памятью через деструкторы в C++.

Ядро Linux имеет очень тонкое и точно настроенное управление памятью. Многие оптимизации производительности привели к ограничению возможностей компиляторов. В ситуации, когда команда разработчиков ядра уже с порога сталкивается с ограничениями компиляторов, внедрение таких автоматических функций может быть рискованным.

Это не только приведет к снижению производительности модулей, использующих RAII, но и создаст новую зависимость. Ядро Linux будет больше зависеть от компилятора. Хотя эта зависимость крайне мала, она становится более серьезной проблемой в масштабах кодовой базы в 30 миллионов строк, работающей на миллиардах устройств по всему миру.

Объектная ориентированность C++ в сравнении с C

«Вы можете писать объектно‑ориентированный код (полезный для файловых систем и т. д.) на C без всей той *** (чуши), которой по сути является C++»

Объектно‑ориентированное программирование (ООП) — это главный аргумент в пользу использования C++ вместо обычного C. Аргумент Линуса здесь заключается в том, что вес и риски, вносимые C++, того не стоят. Он утверждает, что базовое ООП можно реализовать и на C. Это возможно благодаря использованию структур (struct), которые имитируют классы C++. Ниже приведен очень простой пример того, как ООП может быть реализовано на обычном C.

#include <stdio.h>
#include <stdlib.h>

// Определяем "класс" с помощью struct
typedef struct {
    int value;
    // Указатель на функцию в качестве метода
    void (*increment)(struct Person *self);
} Person;

// Реализация метода инкремента
void increment_person(Person *self) {
    self->value++;
}

// Функция-конструктор для инициализации Person
Person* person_new(int initial_value) {
    Person *p = (Person*)malloc(sizeof(Person));
    p->value = initial_value;
    p->increment = increment_person;
    return p;
}

// Функция-деструктор 
void person_free(Person *p) {
    free(p);
}

int main() {
    // Создание экземпляра Person
    Person *p = person_new(5);
    
    // Использование метода
    printf("Initial value: %d\n", p->value);
    p->increment(p);  // Инкремент значения
    printf("After increment: %d\n", p->value);

    // Высвобождение выделенной памяти
    person_free(p);
    return 0;
}

Важно отметить, что ООП не является техническим требованием для функционирования ядра Linux. Это просто другая парадигма или концепция. Целью внедрения ООП в ядро Linux было бы улучшение модульности, инкапсуляции и повторного использования кода, что упростило бы сопровождение и разработку. Все эти преимущества относятся к категории эргономики разработки.

Ядро Linux широко распространено на миллиардах устройств по всему миру благодаря своей производительности и стабильности. Ради этих преимуществ приходится жертвовать эргономикой и скоростью разработки. Любое внедрение улучшенной эргономики неизбежно приведет к снижению производительности и стабильности ядра. Производительность, стабильность и эргономика — это всегда компромисс.

В процессе разработки ядра Linux разработчики часто пренебрегают эргономикой в пользу максимальной стабильности и производительности. Такова философия ядра. Внедрение ООП — это не просто риск или угроза, а полный разворот философии. И, как верно заметил Линус, пользователи Linux, особенно в критически важных системах, не будут этого ни приветствовать, ни терпеть.

Стабильность библиотек и зависимостей в C++

«Бесконечное количество боли, когда они не работают (и любой, кто говорит мне, что STL и особенно Boost стабильны и портируемы, просто настолько полон *** (чепухи), что это даже не смешно)».

Boost и STL могут считаться «стабильными» в контексте пользовательского уровня или разработки приложений пользовательского пространства. Однако, когда речь заходит о разработке ядра и требованиях, предъявляемых к ядру Linux, термин «стабильный» приобретает гораздо более строгое значение.

Предложение использовать C++ со стандартной библиотекой шаблонов (STL) или библиотеками Boost снова направлено на улучшение эргономики разработки. Однако, внедрение таких зависимостей сопряжено с теми же рисками и угрозами для ядра, что и RAII. Они также будут реализованы за счет производительности и стабильности.

Помимо вопросов производительности и стабильности, существует также проблема ответственности. С каждой новой зависимостью, внедренной в ядро, на мейнтернеров этой зависимости накладывается огромная ответственность. Однако вместе с ответственностью передаются и права собственности, что может повлечь за собой риски для безопасности. Например, как это произошло с уязвимостью CVE-2024–3094, когда в библиотеке liblzma был обнаружен бэкдор, созданный таинственным пользователем по имени «Jia Tan».

Неэффективная и раздутая абстракция

«Неэффективные абстрактные модели программирования, с которыми года через два вы можете обнаружить, что какая‑то абстракция была недостаточно эффективной, но весь ваш код уже зависит от множества связанных с ней красивых объектных моделей, и вы не можете исправить эту проблему, не переписывая все приложение».

Эти слова обращены не конкретно к C++, а скорее относятся к ООП и концепции наследования вцелом. Паттерны проектирования в объектно‑ориентированном программировании предназначены для предоставления типовых решений часто встречающихся проблем. Они могут быть очень полезны, и каждый, кто занимается ООП, должен по крайне мере быть иметь о них представление.

«Осмелишься ли ты войти в мою абстрактную фабрику?» (Источник: /r/ProgrammerHumor на Reddit)
«Осмелишься ли ты войти в мою абстрактную фабрику?» (Источник: /r/ProgrammerHumor на Reddit)

Разработчики ядра — талантливые инженеры‑программисты. Они знают, где, когда и как стоит применять ООП. Однако они также осознают, что у этого подхода есть свои недостатки. Помимо технических сложностей, таких как более потребление памяти и большие двоичные файлы, существуют логические подводные камни, от которых не застрахованы даже опытные программисты.

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

Независимо от того, насколько хорош ваш код с технической точки зрения, если вы допустите ошибку в логике иерархии, структуры или архитектуры классов, вы можете оказаться в итоге с совершенно неподдерживаемым кодом. Настолько, что логическая структура классов потребует полного пересмотра. Нередко ООП‑приложениям приходится полностью перестраивать свою архитектуру уже через пару лет. Вероятность, что ядро Linux может пойти по этому пути, ненулевая. Стоит ли рисковать?

В итоге вы все равно получите чистый C

«Другими словами, единственный способ сделать хороший, эффективный, системный и портируемый C++ — это ограничиться всем тем, что в принципе доступно на C».

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

«Ограничение проекта языком C означает, что люди не смогут его испортить. Это также означает, что вы получите много программистов, которые действительно понимают низкоуровневые проблемы и не будут портить проект идиотской *** (чушью) „объектной модели“».

Второй аргумент, который он приводит в последнем абзаце своего сообщения в рассылке от 6 сентября 2007 года, касается фокуса на определенных разработчиках при продолжении использования чистого C. Суть этого аргумента заключается в том, что язык C привлекает разработчиков ядра, которые будут сосредоточены на аппаратном обеспечении и решении актуальных задач, не отвлекаясь на прелести и запутанность ООП.

Разработчики — тоже люди, и этот аргумент подчеркивает важность человеческого фактора в программировании. Он показывает, что Линус, как BDFL (Benevolent Dictator for Life) мира Linux, должен думать об управлении людьми так же, как и любой другой лидер. Он является основателем, возможно, самого большого сообщества разработчиков на планете и должен поддерживать его в таком состоянии. В какой‑то степени ему приходится учитывать ожидания, надежды и мечты более 20 миллионов разработчиков со всего мира и управлять ими.

Что дальше, Rust, Go или даже Java?!

«В конечном счете, C — очень простой язык. Это одна из причин, почему мне и многим программистам нравится C. Однако, другая сторона медали заключается в том, что из‑за простоты в нем также очень легко совершать ошибки, а в Rust — нет». — Линус Торвальдс в беседе с Дирком Хохнделем

Неудивительно, что уже звучат обсуждения возможности использования Rust в ядре Linux (к чему и было это упоминание). Кто знает, что будет дальше: Go, C# или даже Java, скомпилированная в собственный бинарник с помощью GraalVM? Намерения разработчиков, задающих эти вопросы, понятны. Они похожи на те, что были с C++, и в них нет ничего дурного.

Линус должен где‑то провести черту. Linux на 99% состоит из чистого C, а остальное — чистый Ассемблер. Архитектура ядра — самая чистая из всех ядер операционных систем в истории человечества. Проблемы ядра связаны не только с программированием, но и с техническими аспектами, такими как обработка прерываний, переключение контекста и другими низкоуровневыми операциями процессора.

Ядро должно предоставлять множество важных функций, включая драйверы устройств, файловые системы, выдающийся сетевой стек Linux, управление памятью, планирование процессов и многое другое. Все эти задачи являются технически сложными, и добавление любых новых функций, которые не будут напрямую связаны с этими задачами и не принесут пользы конечным пользователям, весьма сомнительно. Использование кода Rust в ядре Linux добавляет гораздо больше сложностей, чем это сделал бы C++, поэтому ответ на этот вопрос вполне очевиден — «нет».

Оправдан ли отказ от C++?

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

«Открытые операционные системы имеют не одни только преимущества», — пропаганда Microsoft, 2004.
«Открытые операционные системы имеют не одни только преимущества», — пропаганда Microsoft, 2004.

Успех Linux основан на его высокой производительности и стабильности, которые стали возможными благодаря высоко оптимизированному на коду, состоящему на 99% из языка C и лишь 1% ассемблера. Внедрение C++, обработки исключений, объектно‑ориентированного программирования или любых других новых и непроверенных технологий, вероятно, негативно сказалось бы на обеих этих системах. Хотя это звучит смешно — называть C++ новой и непроверенной технологией, но для ядра Linux это так.

Чему мы можем вынести для себя из отказа Линуса от C++

Ядро Linux — это уникальное достижение программной инженерии мирового масштаба. Более 30 миллионов строк кода работают на миллиардах машин. Его производительность и стабильность не имеют себе равных. Это результат упорного труда и преданности разработчиков ядра. Продолжающиеся споры о systemd и SysVinit демонстрируют, насколько эмоциональными, но в то же время технически проработанными могут быть крупные изменения в Linux.

Ад зависимостей, который часто встречается в приложениях Node.js, — полная противоположность тщательно продуманным 30 миллионам строк кода, составляющим ядро Linux. Основным выводом из дискуссий о применении C++ в ядре Linux может стать то, что мы должны тщательно взвешивать и анализировать решения, касающиеся зависимостей, учитывая долгосрочные последствия.

Программистам следует быть более осторожными с зависимостями
Программистам следует быть более осторожными с зависимостями

Кроме того, подход Линуса к разработке ядра на C++ или даже Rust демонстрирует, насколько важен выбор «правильного инструмента для работы. Использование одного чистого C позволило стандартизировать ядро Linux, но оно по‑прежнему сталкивается с проблемами, над которыми разработчики ядра упорно трудятся каждый день. Когда завтра ваш коллега предложит реализовать какую‑то функцию вашего Go‑приложения на Bython, вы можете рассмотреть подход Линуса.

Заключительный аргумент

В следующий раз, когда вам предстоит проходить МРТ в больнице, подумайте о том, хотите ли вы, чтобы томограф мог пробрасывать исключения. Конечно, забавно наблюдать, как «умные холодильники с искусственным интеллектом» обретают самосознание и начинают подъедать ваши продукты. Но если МРТ начнет действовать по своему усмотрению, это может стать смертельно опасным.

Хотите ли вы, чтобы ядро магнитно‑резонансного томографа выбрасывало исключения?
Хотите ли вы, чтобы ядро магнитно‑резонансного томографа выбрасывало исключения?

Сегодня ядро Linux используется во многих устройствах, включая компьютеры, телефоны, серверы, сетевое оборудование, встраиваемые системы, игровые консоли, медицинские приборы, бытовую технику, носимые устройства, автомобили, светофоры, промышленное оборудование, космические аппараты, системы управления железными дорогами, банкоматы, системы управления воздушным движением, спутники, научные приборы, медицинское оборудование, сельскохозяйственную технику, космические телескопы, подводные лодки, истребители и крылатые ракеты. Это лишь некоторые из них. Готовы ли вы пойти на риск сломать их?

Хотя Линус, вероятно, не в восторге от необходимости снова и снова отвечать на вопрос о C++ в разработке ядра, его все же стоит задать и рассмотреть. И хоть ответ отрицательный, мы можем извлечь из него важные уроки: Это модно или необходимо? Я делаю это для себя или для своих пользователей? Каковы долгосрочные последствия этого?


Материал подготовлен в рамках онлайн-курса «Разработка ядра Linux». На странице курса можно ознакомиться с подробной программой, а также посмотреть записи открытых уроков.

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

  • 24 апреляRust, Си ABI и линковка
    Где заканчивается контроль над кодом и начинается доверие к компилятору? Разбираемся на стыке языков.

  • 12 маяB-tree индексы: как сделать PostgreSQL быстрее
    О том, как простая, но точная инженерия выигрывает у модных, но тяжёлых решений — на примере одной из самых критичных подсистем СУБД.

Теги:
Хабы:
+23
Комментарии164

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS