Вебинар: C++ и неопределённое поведение - 27.02
Вторая часть истории о том, как C и C++ стали такими, какими мы их знаем сегодня. Здесь расскажем об официальной сертификации C и C++, а также о вышедших инструментах в период с 1991 года и до наших дней.
В 1994 году создатель языка C++ Бьёрн Страуструп публикует книгу о том, как создавался язык C++ — "Дизайн и эволюция C++". Особое внимание в ней автор уделяет общим принципам дизайна, а также ходу эволюции языка от C With Classes до современного состояния с описанием того, что влияло на принятие тех или иных архитектурных решений. В книге также описаны многие языковые концепции, без которых сейчас представить C++ довольно сложно.
Рисунок N1 — обложка первого издания книги "Дизайн и эволюция C++"
Разработка библиотеки Qt началась в 1991 году с амбициозной целью — создать "лучшую в мире библиотеку реализации графического интерфейса на C++". Этот проект был задуман в ответ на растущие потребности в эффективных и гибких инструментах для разработки графических интерфейсов, которые бы интегрировались с платформами на основе C++ и позволяли бы разработчикам сосредоточиться на функциональности приложений.
На ранних этапах создания библиотеки особое внимание уделялось удобству работы с графическим интерфейсом, а также созданию архитектуры, которая позволяла бы использовать её в различных операционных системах без значительных изменений в коде. В 1992 году разработчики представили парадигму, основанную на концепциях "сигналов" и "слотов". Эта идея кардинально изменила подход к обработке событий в программировании и позволила существенно упростить связь между объектами в графическом интерфейсе. Позднее, в 1993 году, эта концепция уже была реализована в коде.
В том же 1993 году было готово первое графическое ядро библиотеки, и разработчики приступили к созданию визуальных компонентов, которые стали основой для построения пользовательских интерфейсов.
20 мая 1995 года состоялся первый публичный релиз библиотеки — Qt 0.90.
В марте 2009 года была представлена первая версия Qt Creator — интегрированной среды разработки, предназначенной для упрощения работы с библиотекой Qt.
Сегодня Qt является одним из самых всеобъемлющих фреймворков на C++. Он активно используется в различных областях, включая создание десктопных и мобильных приложений, а также в разработке программного обеспечения для встраиваемых систем.
Первый стандарт C++, вышедший в 1998 году и получивший название C++98, был призван стандартизировать весь тот функционал, что добавлялся в язык с момента его появления. Annotated C++ Reference Manual ранее пролил свет на функционал, который был реализован с выходом этого стандарта.
Было стандартизировано использование шаблонов, позволяющее создавать общие классы и функции, работающие с любыми типами данных. Также было стандартизировано использование исключений.
Во избежание конфликтов имён были добавлены namespace
'ы, которые позволили объединить такие сущности как классы, объекты и функции в отдельной области видимости. Для представления логических значений был добавлен тип данных bool
и ключевые слова true
и false
.
Было стандартизировано поведение множественного наследования и виртуальных функций для обеспечения единообразия между компиляторами. Также были стандартизированы операторы new
и delete
.
Стандартизировали и стандартную библиотеку, в частности стандартную библиотеку шаблонов, библиотеку I/O потоков, библиотеку комплексных чисел, контейнеры (вектор, двусвязный список, двунаправленный список, множество, отображение), адаптеры (стек, очередь, приоритезированная очередь), итераторы, алгоритмы, функциональные объекты.
После принятия C++98 многие остались недовольны отсутствием некоторого функционала в STL. В связи с этим появился новый проект — Boost — собрание библиотек классов, использующих функциональность языка C++ и предоставляющих высокоуровневый кроссплатформенный интерфейс для решения различных задач.
Данный проект является своеобразным испытательным полигоном для различных расширений языка и библиотек, которые могут быть включены в следующем стандарте.
В 1998 году вышла версия 1.0 игрового движка Unreal Engine. На тот момент он был одним из первых универсальных движков, совмещавших в себе графический и физический движки, искусственный интеллект, управление файловой и сетевой системами, а также готовую среду для разработки игр.
Unreal Engine 1.0 включал в себя действительно революционные для того времени технологии, например, использование динамического графа сцены, благодаря которому стало возможным добавлять ряд эффектов для наложения на поверхности.
Первая версия движка вышла для платформ Windows и Macintosh, однако чуть позже он был успешно использован на GameCube, PlayStation 2 и Xbox. Сегодня Unreal Engine поддерживает большую часть платформ: Windows, PlayStation 4 и 5, Xbox Series, Xbox One, Nintendo Switch, macOS, iOS, Android, SteamVR, Oculus, Linux, SteamDeck и другие.
Уже через год вышла улучшенная версия движка (которую ещё называли Unreal Engine 1.5) со значительными дополнениями в функционал. Среди них: поддержка лицевой анимации для персонажей, увеличенное до 1024×1024 максимальное разрешение текстур и расширяемая система частиц.
После принятия первого стандарта для языка C он оставался относительно неизменным на протяжении некоторого времени. Но в 1999 году был опубликован новый стандарт ISO/IEC 9899:1999.
В нем были добавлены некоторые возможности, которые ранее реализовывались в качестве расширений для некоторых компиляторов.
Были добавлены встраиваемые (inline
) функции, при вызове которых компилятор будет подставлять тело функции вместо её вызова.
Появилось несколько новых типов данных: long long int
, дополнительные расширенные целые типы, явный логический тип данных _Bool
, а также комплексный тип _Complex
для представления комплексных чисел.
Были добавлены массивы переменной длины — массивы, длина которых вычисляется на этапе выполнения программы. Поддержаны однострочные комментарии (//
) как в BCPL и C++, а также добавлены вариативные макросы (макросы переменной арности) и составные константы.
Данный стандарт был принят в качестве стандарта ANSI в мае 2000 года.
Эта система сборки изначально разрабатывалась для проекта Insight Toolkit, спонсируемого Национальной медицинской библиотекой США (NLM). Проект требовал поддержки множества платформ. Для разработки этого инструментария NLM объединила Kitware с другими компаниями и университетами. Полученная система была названа CMake и позволила значительно упростить процесс сборки и управления проектами. Буква C в названии системы, кстати, означает "кроссплатформенность".
CMake автоматически генерирует файлы сборки для разных операционных систем и компиляторов, что стало особенно актуальным для крупных проектов, требующих универсальности и гибкости. Вскоре CMake приобрела широкую популярность в мире разработки программного обеспечения благодаря своей способности работать с различными платформами, поддерживать сложные зависимости и интеграции, а также обеспечивать высокую степень автоматизации в процессе сборки.
В 2003 году был опубликован стандарт C++03. Однако он включал не так много новых функций. C++03 является логическим дополнением стандарта C++98, исправляя его ошибки и недочёты. В ходе его разработки было рассмотрено множество отчётов о проблемах, которые и были исправлены стандартом. Единственное, что было добавлено непосредственно в язык с C++03 — инициализация значением (value initialization) — инициализация, выполняющаяся, когда объект создаётся с пустым инициализатором.
Однако в 2005 году был опубликован документ Draft Technical Report on C++ Library Extensions с описаниями предложений о дополнениях в стандарт библиотеки C++.
Например, в данном документе были предложены:
reference_wrapper
— класс, создающий оболочку для ссылки;function
, bind
, result_of
, mem_fn
;tuple
для кортежей;array
для массивов фиксированной длины;regex
для работы с регулярными выражениями.В 2007 году данный документ будет опубликован в качестве стандарта ISO/IEC TR 19768:2007, а предложенные в нём функции найдут своё применение в языке чуть позднее.
В 1987 году в рамках проекта GNU был разработан набор компиляторов GCC. Изначально инструмент, названный GNU C Compiler, поддерживал только компилятор для языка C, но позже появилась поддержка и других языков, например C++, и его полным названием стало GNU Compiler Collection.
В 2007 году в качестве альтернативы GCC появился Clang — транслятор для C-подобных языков, работающий на базе фреймворка LLVM.
Одна из задач этого проекта заключалась в реализации инкрементной трансляции, которая позволила бы более тесно интегрировать компилятор с интерфейсом среды разработки, с чем в GCC были проблемы. Модульный дизайн Clang позволяет эффективно использовать его в IDE для индексирования кода, подсветки синтаксиса и подобного.
Также отличием Clang от GCC стало то, что упор в GNU Compiler Collection был сделан на генерацию кода, тогда как Clang являлся полноценным универсальным фреймворком для парсинга, индексации, статического анализа и компиляции. А на этапе парсинга Clang не делал упрощений в исходном коде, точно воспроизводя его в абстрактное синтаксическое дерево.
В проекте на сегодняшний день участвуют в том числе разработчики из Google и Apple.
31 декабря 2006 года (без нескольких часов в 2007) состоялся первый публичный релиз анализатора Viva64, который был предназначен для поиска ошибок при миграции на 64-битные системы.
В 2008 году выйдет уже вторая версия анализатора, а третья, датируемая 2009, объединит Viva64 и VivaMP (инструмент для анализа проблем в многопоточных программах, построенных на OpenMP) в один инструмент — PVS-Studio.
Сегодня PVS-Studio — это статический анализатор и SAST-решение для C, C++, C# и Java. Попробовать инструмент можно по этой ссылке.
Новая версия стандарта языка C++ вышла в 2011 году. В неё вошли изменения в ядре языка, была расширена стандартная библиотека, в которую были включены все предложения из TR1, кроме специальных математических функций.
Примечание. О нововведениях стандартов C++, начиная с C++11, мы выпускали полноценные лекции в видеоформате. Посмотреть их можно по этой ссылке.
В ядре была добавлена поддержка многопоточности, улучшена поддержка обобщённого программирования, унификация инициализации, а также проведены работы по повышению производительности. Например, появилась семантика перемещения — совокупность семантических правил и средств языка C++, предназначенных для перемещения объектов, время жизни которых скоро истечёт, вместо их копирования. Она позволяет избежать дорогостоящего копирования. Для её реализации были добавлены rvalue и forwarding ссылки, конструкторы перемещения и перемещающий оператор присваивания, а в стандартной библиотеке шаблонов были добавлены функции для поддержки семантики перемещения, например, std::move
и std::forward
.
В этом стандарте появились спецификаторы типа заполнителя auto
, показывающие, что тип объявляемой переменной будет автоматически выведен из её инициализатора. Также появился спецификатор decltype
, позволяющий вывести тип объявленной сущности или тип и категорию значения переданного выражения.
Появилась возможность объявить функции как удалённые с помощью = delete;
, а также заставить компилятор генерировать тела специальных функций с помощью = default;
. В языке появились лямбда-выражения.
Были добавлены спецификаторы final
и override
при объявлениях классов и виртуальных функций. Оба заставляют компилятор проверить, происходит ли переопределение какой-либо виртуальной функции из базового класса. Это позволяет избавиться от частой ошибки в C++, когда вместо переопределения виртуальной функции из базового класса программист на самом деле скрывал её, добавляя новую функцию. Также первый спецификатор дополнительно может быть применён к определению класса, и тогда от такого класса будет запрещено наследоваться.
Были добавлены тип std::nullptr_t
и литерал nullptr
этого типа. С помощью литерала теперь можно выставить состояние указателя в нулевой вместо применения макроса NULL
. Также он позволяет избежать неоднозначности при вызовах функции, когда существуют две перегрузки с интегральным типом и указателем. Подобный литерал позже появится в языке C, но только со стандартом C23.
Также появился спецификатор constexpr
, позволяющий перенести вычисление значения функции на этап компиляции, а далее использовать лишь там, где разрешены только константные выражения времени компиляции. Рекомендую также эту отдельную статью, в которой рассказывается о дизайне и эволюции constexpr
в C++.
Был добавлен цикл for
для диапазонов (range-based for). Он позволяет проитерироваться по объекту (например, стандартному контейнеру или пользовательскому типу) при условии, что для типа определены функции-члены begin
и end
. Эти функции должны возвращать итераторы на первый элемент и на элемент, следующий за последним. Причём данная возможность была добавлена из библиотеки Boost.Foreach. Оттуда же был добавлен и static_assert
(Boost.StaticAssert), позволяющий проверять утверждения на этапе компиляции.
В стандартной библиотеке также произошли изменения. Например, вот что было добавлено:
std::unordered_set
, std::unordered_map
и их multi-версии;std:array
и std::forward_list
;std::unique_ptr
, std::shared_ptr
, std::weak_ptr
;std:: linear_congruential_engine
, std::mersenne_twister_engine
, std:: subtract_with_carry_engine
), адаптеры (std::discard_block_engine
, std:: independent_bits_engine
, std:: shuffle_order_engine
) и функции распределения (равномерное, нормальное, Пуассона, Бернулли);std::thread
, std::mutex
, std::lock_guard
, std::unique_lock
, std::condition_variable
, std::future
, std::promise
, ....Примечание. Недавно мы выпускали статью о том, почему std:array
в C++ не медленнее, чем массив в C. Прочитать её можно по этой ссылке.
18 августа 2014 года вышел стандарт C++14, задуманный как расширение для предыдущего стандарта — C++11. Хоть об этом стандарте говорят как о минорном, но всё же список изменений не такой маленький, как может показаться.
Причин на это несколько. Во-первых, в данном стандарте был отполирован C++11 и исправлены различные дефекты (в общей сложности 276 ядровых и 158 библиотечных). Во-вторых, были добавлены шаблоны переменных, позволившие определять семейство переменных или статических элементов данных. Помимо этого, были добавлены возможности, без которых сейчас писать код на C++ сложновато, например:
auto
для возвращаемых типов функций;decltype(auto)
;constexpr
-функций, что дало возможность вычислять на этапе компиляции почти любой код;Ну и киллер-фича C++14 — стало возможным устранять или объединять некоторые динамические аллокации. В первом случае компилятор теперь может предоставить буфер без вызова заменяемой функции аллокации, например, на стеке. Во втором случае при вычислении выражения new e1
компилятор может выделить память сразу и для другого выражения new e2
. Для этого требуется выполнение трёх условий:
e1
, строго содержит время жизни объекта, выделенного e2
.e1
и e2
должны вызывать одну и ту же заменяемую глобальную функцию аллокации;e1
и e2
будут перехвачены одним и тем же обработчиком.Новый стандарт вышел в декабре 2017 года. Пройдёмся по списку возможностей обновлённого стандарта.
Появились выражения свёртки (fold expressions). Свёртка — элемент синтаксиса C++, предназначенный для свёртки пакетов параметров вариативных шаблонов с необязательным начальным значением. Её использование позволяет избегать громоздких рекурсивных вызовов, а также записывать применение операций ко всем отдельным аргументам пакета в компактном виде. Также для шаблонов была добавлена возможность писать auto
для non-type template параметров.
Были добавлены конструкции if
и switch
с инициализатором. Помимо этого, появился if constexpr
, который позволяет компилировать только одну ветку кода и уменьшить число мест, требующих SFINAE. В constexpr
также стало возможным использовать лямбда-выражения и объявлять их как constexpr
. Был добавлен лямбда-захват *this
, который копирует состояние текущего объекта.
Появился механизм структурного связывания (structured binding), который позволил удобно декомпозировать пары, кортежи и другие подобные объекты.
Изменения стандарта коснулись и стандартной библиотеки. В неё были добавлены новые типы, инструменты для управления памятью и программирования времени компиляции.
Примечание. В 2017, когда был выпущен стандарт, мы публиковали статью с подробным разбором нововведений C++17. Прочитать её можно по ссылке.
В декабре 2020 года, спустя ещё один трёхлетний цикл, вышел стандарт C++20.
В C++ 20 появились ограничения, позволяющие определить требования к аргументам шаблона. Именованные наборы ограничений были названы концептами. Нарушения ограничений обнаруживаются на этапе компиляции, из-за чего детектирование ошибок становится довольно лёгким.
В языке появились модули, позволившие обмениваться объявлениями и определениями между единицами трансляции. Также были добавлены сопрограммы (корутины) — функции, которые могут остановить выполнение, чтобы возобновить его позже.
Появился spaceship-оператор (<=>
), который позволил переложить на компилятор сравнение полей. Добавление такого функционала привело к появлению non-type template parameter для пользовательских типов.
Для constexpr
были разрешены некоторые возможности, например, вызов виртуальных функций или использование dynamic_cast
. Также появились спецификаторы consteval
и constinit
. Первый указывает, что каждый вызов функции должен создавать константу времени компиляции. Второй указывает, что переменная инициализируется со static storage duration или thread storage duration константой времени компиляции, но затем её можно модифицировать, в отличии от constexpr
.
Появились constexpr
аллокаторы, что позволило в пределах constexpr
-вычисления использовать даже контейнеры, требующие выделения памяти. При этом динамическая аллокация не может покинуть пределы constexpr
-функции.
Были добавлены макросы проверки функциональности. Их функция состоит в простом и переносимом способе проверки возможностей языка, представленных, начиная со стандарта C++11.
Диапазонные циклы for
, начиная с C++20, обзавелись возможностью предварительной инициализации, а лямбда-выражения получили пакетные расширения в лямбда-захват.
C++23 вышел в октябре прошлого года. Процесс разработки этого стандарта отличался от предыдущих, поскольку почти все заседания комитета проходили дистанционно.
В этом стандарте появились явные функции-члены объекта (explicit object member functions). Все нестатические функции в классах всегда принимают дополнительный параметр, которым является сам объект (this
-указатель). До C++23 он принимался функцией неявно, а в новом стандарте появилась возможность явно указывать этот параметр при объявлении функции.
Для оптимизации времени компиляции был добавлен if consteval
, который позволяет проверить, выполняется ли функция в константном контексте.
Лямбда-выражения получили поддержку атрибутов, а для многомерных массивов появились многомерные индексы.
Появилась возможность маркировать недостижимый код с помощью std:unreachable
, помогая оптимизации компилятора.
В стандартной библиотеке было улучшено форматирование строк, были добавлены новые адаптеры std::flat_map
и std::flat_set
.
Вы могли обратить внимание, что ни одного стандарта для C после C99 в этой статье не было описано. Почему? Дело в том, что все следующие стандарты языка были сфокусированы на доработке тех возможностей языка, что выходили ранее, а также на дополнении стандартной библиотеки.
Самым заметным с точки зрения непосредственно языковых возможностей можно назвать стандарт C11, поскольку в нём были произведены некоторые изменения. Например, были добавлены типо-обобщённые выражения с использованием ключевого слова _Generic
и поддержка многопоточности.
Вместе с C++23 в 2024 году вышел и C23. В этом стандарте убрали формат определения функций из K&R. Недостатком такого синтаксиса была невозможность компилятором проверить соответствие типов аргументов при вызове функций. Данное изменение привело к большей схожести между C и C++.
Помимо этого, в языке появились некоторые возможности, которые ранее были добавлены в C++:
static_assert
как ключевое слово вместо _Static_assert
, введённого в C11;thread_local
как ключевое слово вместо макроса, раскрывавшегося как _Thread_local
;bool
с предопределёнными значениями true
и false
;auto
;realloc
теперь не определено при размере аллокации, равном 0;nullptr
и nullptr_t
.Также появилась директива #embed
, позволяющая легко включать двоичные данные в исполняемый образ программы без необходимости внешнего скрипта.
Здесь наше путешествие во времени окончено, и мы снова в настоящем. C и C++ — поистине культовые языки, которые, несмотря на свой возраст, и по сей день остаются на вершине списка популярных языков программирования. Надеюсь, такое путешествие в историю было интересно и вам.
0