Синтаксис си/с++

Стандарты C++: от C++98 до C++23, ключевые изменения

Введение

Весь предыдущий цикл неоднократно отмечал, что та или иная возможность появилась «начиная с C++11» или «C++17» — в этой статье систематизируем эволюцию стандартов C++ от исходного C++98 до современного C++23, давая общую картину того, какие ключевые возможности появились на каждом этапе.

Концепция

C++ развивается через регулярно публикуемые стандарты ISO, каждый из которых добавляет новые языковые возможности и расширения стандартной библиотеки, обычно сохраняя обратную совместимость с кодом, написанным для более ранних стандартов (хотя и не абсолютную — некоторые устаревшие возможности постепенно выводятся из стандарта). C++98/03 — исходный, классический стандарт, заложивший основу языка (классы, шаблоны, исключения, базовая стандартная библиотека контейнеров). C++11 — крупнейшее, революционное обновление, добавившее move-семантику (статья 546), лямбды (статья 545), auto (статья 544), умные указатели (статья 547), и многое другое, фактически создавшее «современный» C++ в его узнаваемом сегодня виде. Последующие стандарты (C++14, 17, 20, 23) добавляли более инкрементальные, хотя порой существенные улучшения — структурированные привязки и std::optional (C++17, статья 550), concepts и ranges (C++20, статья 549), и продолжающееся развитие в C++23.

Пример кода: Стандарты C++

// C++98/03 — классический стиль, без современных удобств
#include <vector>
#include <iostream>

int main()
{
    std::vector<int> numbers;
    numbers.push_back(1);
    numbers.push_back(2);

    for (std::vector<int>::iterator it = numbers.begin(); it != numbers.end(); ++it) {
        // ДЛИННЫЙ, явный тип итератора — auto появится только в C++11
        std::cout << *it << " ";
    }
    return 0;
}
// C++11 — тот же код, с современными удобствами
#include <vector>
#include <iostream>

int main()
{
    std::vector<int> numbers = {1, 2}; // список инициализации (initializer list) — новое в C++11

    for (auto it = numbers.begin(); it != numbers.end(); ++it) { // auto вместо длинного типа
        std::cout << *it << " ";
    }

    for (const auto &num : numbers) { // range-based for — значительно короче явных итераторов
        std::cout << num << " ";
    }
    return 0;
}
// C++17/20 — структурированные привязки, concepts (статьи 549-550)
#include <map>
#include <iostream>

int main()
{
    std::map<std::string, int> data = {{"a", 1}};
    for (const auto &[key, value] : data) { // структурированная привязка — C++17
        std::cout << key << "=" << value << std::endl;
    }
    return 0;
}

Пояснения к коду

Сравнение C++98/03-стиля (явный, многословный тип итератора) и C++11-стиля (auto, range-based for) для одной и той же задачи перебора std::vector наглядно демонстрирует, насколько существенно C++11 изменил повседневный, идиоматичный стиль написания кода — задача осталась той же, но синтаксис стал значительно компактнее и читабельнее. Список инициализации {1, 2} для numbers показывает ещё одно удобство C++11, отсутствующее в более ранних стандартах, где аналогичная инициализация вектора требовала бы либо отдельных вызовов push_back(), либо менее удобных обходных техник. Структурированная привязка в третьем примере (статья 550) показывает продолжение этой же общей тенденции — каждый новый стандарт добавляет инструменты, делающие типичный, повседневный код всё более компактным и явно выражающим намерение разработчика, без необходимости в обходных, многословных конструкциях более старых стандартов.

Подводные камни

  • Написание нового кода в устаревшем, «C++98-стиле» без использования возможностей более новых стандартов, доступных для используемого компилятора и целевой платформы — это не делает код некорректным, но лишает его преимуществ современных стандартов (читаемость, безопасность, иногда и производительность через move-семантику), и для нового кода (если только не существует специфичного, обоснованного ограничения — поддержка очень старого компилятора, специфичная embedded-платформа с ограниченной поддержкой стандартов) стоит ориентироваться на возможности актуального, используемого проектом стандарта C++, а не писать в устаревшем стиле «по привычке».
  • Предположение о доступности конкретной возможности нового стандарта без проверки реальной поддержки используемым компилятором и его настроенной версии стандарта — не все компиляторы поддерживают абсолютно все возможности самого новейшего стандарта одновременно с его публикацией (особенно специфичные, embedded-ориентированные компиляторы, статьи 351-360, часто отставание в поддержке новейших стандартов), и явная проверка реальной поддерживаемой версии стандарта (__cplusplus макрос, или явная документация используемого компилятора) важна перед тем, как полагаться на конкретную, недавно появившуюся возможность.
  • Игнорирование изменений в поведении или удаления отдельных возможностей между стандартами (а не только добавления новых) — хотя обратная совместимость в целом сохраняется, отдельные возможности были помечены как устаревшие (deprecated) и впоследствии удалены из стандарта в более новых версиях (например, некоторые устаревшие способы работы со строками, упомянутый ранее register спецификатор статьи 513), и обновление компилятора/стандарта для существующего, давно написанного проекта иногда требует адаптации кода, использующего такие устаревшие, впоследствии удалённые возможности.
  • Смешивание явного указания версии стандарта в системе сборки (CMake, основной цикл статей) с реальным, фактически использованным в коде синтаксисом более новой или более старой версии — если система сборки настроена на использование, например, C++14, а код использует синтаксис, требующий C++17 или C++20 (как concepts статьи 549), компиляция завершится ошибкой, и явное, согласованное указание реально нужной версии стандарта в конфигурации сборки (set(CMAKE_CXX_STANDARD 20) для CMake) должно соответствовать реально используемым в коде возможностям конкретного стандарта.