Написать пост

Что нового в C++ 20 и какие изменения ожидают язык в будущем?

Аватар Klara Oswald

Статья кратко расскажет об основных нововведениях в C++ 20, а также о том, какие изменения в языке можно ожидать в будущем.

Те, кто знаком с C++, обычно делятся на два лагеря: одни любят его за обширность и универсальность, другие ненавидят за раздутую сложность и предпочитают придерживаться альтернативных языков. В любом случае, сейчас у вас есть шанс пересмотреть своё мнение о С++. Недавно был собран комитет по стандартам языка, чтобы завершить работу над последней версией языкового стандарта (C++ 20) и определить новую функциональность, которая появится в следующем крупном релизе.

После C++ 17 это будет шестая редакция стандарта. Этот язык прошёл долгий путь от своего бытия «надмножеством C». Программирование на C++ включает в себя множество стилей: от простейшего «C с классами» до написания кода, который выглядит как произведение искусства. Список новых возможностей длинный, а объединённых предложений по спецификациям ещё больше. Каждого из этих дополнений хватит на свою собственную статью.

Для общего представления о том, что будет с C++ в следующем году, вкратце рассмотрим некоторые из новых функций, изменений и дополнений, которые ждут в C++ 20. От улучшенной проверки типов и сообщений об ошибках компилятора до Python-подобной обработки строк и планов по замене системы #include.

Делаем язык безопасней

Когда язык меньше ограничивает детали реализации, это обеспечивает большую гибкость для разработчиков. А также большой потенциал для «недоразумений», которые в будущем могут привести к ошибкам. По сей день это самая большая ценность и одновременно слабость C, и C++ всё ещё достаточно похож на него корнями. Ограничения — действенное, но непопулярное решение. Хорошо, что в C++ есть компромиссы, которые оставляют гибкость на уровне языка и добавляют ограничения на усмотрение разработчика.

Рекомендации компилятору: явные константы

Ещё в C++ 11 было введено ключевое слово constexpr как дополнение к обычному объявлению const, определяющему константное выражение, которое можно вычислять во время компиляции. Это открывает множество возможностей оптимизации для компилятора, например позволяет явно заявить, что функция будет возвращать постоянное значение. Это помогает более чётко показать намерение функции, избегая потенциальных проблем в будущем. Например:

			int foo() {
    return 123;
}
 
constexpr int bar() {
    return 123;
}
 
const int first = foo();
const int second = bar();
		

Технически между этими двумя функциями выше нет разницы, и любая из них будет возвращать константное значение, допустимое для присваивания переменной типа const. Разница лишь в том, что функция bar() делает это в явном виде. В случае foo() это скорее побочный эффект и без полного контекста не очевидно, что возвращаемое значение функции должно быть константой. Использование constexpr устраняет любые сомнения и позволяет избежать возможных побочных эффектов, что сделает код более стабильным в долгосрочной перспективе.

constexpr подвергался нескольким переработкам ещё до релиза C++ 20. Особенно это касается снятия ранее существующих ограничений на использование этих переработок. Самое главное, новый стандарт позволяет использование функций типа virtual constexpr. Разработчики могут использовать try/catch внутри constexpr (при условии отсутствия исключений внутри) и менять члены внутри объединения.

Более того, std::string, std::vector, а также множество других ранее пропущенных в стандартной библиотеке функций будут полностью использовать constexpr. Если необходимо проверить, действительно ли фрагмент кода выполняется внутри определения константы, сделать это можно с помощью функции std::is_constant_evaluated(), которая возвращает соответствующее логическое значение.

Стоит обратить внимание, constexpr-код подразумевает, что он может быть вычислен во время компиляции и является допустимым константным выражением. Но это не обязательно, и нет гарантии, что вычисление произойдёт во время компиляции, оно может быть отложено на время выполнения. Это в основном важно для проведения оптимизации компилятором и не влияет на поведение программы, но также показывает, что constexpr — это в первую очередь отметка о намерении сделать функцию константной.

			constexpr int foo(int factor) {
    return 123 * factor;
}
 
const int const_factor = 10;
int non_const_factor = 20;
 
const int first = foo(const_factor);
const int second = foo(non_const_factor);
		

В коде выше first будет определяться во время компиляции, поскольку все задействованные выражения и значения являются константами и известны во время компиляции. А second будет определяться во время выполнения, поскольку сам non_const_factor не является константой. Это не меняет того факта, что foo() по-прежнему будет возвращать константное значение, только компилятор пока не может точно определить, какое именно. Чтобы компилятор знал значение, в C++ 20 вводится ключевое слово consteval, чтобы объявить функцию как непосредственную (immediate). Объявление foo() как consteval вместо constexpr теперь вызовет ошибку. Фактически, непосредственные функции действительно известны только во время компиляции и это превращает conteval-функции в альтернативу для макрофункций.

С другой стороны проверки константного выражения находится новое ключевое слово constinit, которое говорит компилятору, что объект будет статически инициализирован с постоянным значением. Если вы знакомы с проблемой порядка статической инициализации, то это попытка её решить.

Константные выражения — не единственные изменения в C++ 20, призванные улучшить проверку времени компиляции и обеспечить стабильность.

Концепция концептов

С технической точки зрения это не совсем новая вещь, концепты (Concepts) превратились из экспериментальной функции в полноценную часть языкового стандарта. Это позволяет добавлять семантические ограничения к шаблонам и в конечном счёте делает общие всплывающие подсказки при программировании более конкретными.

В некоторой степени связанные с особенностями типа (type traits), концепты обеспечивают соответствие используемых в шаблоне данных указанному набору критериев и проверяют это в начале процесса компиляции. Например вместо проверки объекта на is_integral используется объект типа Integral. В результате, если определённое требование концепта не выполнено, компилятор может выдать короткое и значимое сообщение об ошибке вместо дампинга большого количества ошибок и предупреждений откуда-то из глубин кода шаблона, что не будет иметь особого смысла без дальнейшего копания в этом коде.

Компилятор знает, какие данные необходимы. Он также чётко показывает разработчикам, какие данные ожидаются, в первую очередь помогая избежать сообщений об ошибках и избегая недоразумений. С другой стороны концепты можно использовать для ограничения возвращаемого типа шаблонных функций, ограничивая переменные концептом, а не универсальным автоматическим типом, который можно рассматривать как версию void * на C++.

Некоторые базовые концепты будут предоставлены в стандартной библиотеке, и если вы не хотите ждать обновлённых компиляторов, GCC имеет экспериментальные концепты, реализованные ещё с версии 6. Вы можете включить их с помощью параметра командной строки -fconcepts. Обратите внимание, что в первоначальном проекте и текущей справочной документации имена концептов были определены по типу CamelCase, но они будут изменены на snake_case, чтобы сохранить согласованность со всеми другими стандартными идентификаторами.

Диапазоны — новые итераторы

Диапазоны (Ranges) по сути являются итераторами, которые охватывают последовательность значений в коллекциях, таких как списки или векторы. Но вместо того, чтобы постоянно перемещать начало и конец итератора, диапазоны просто сохраняют их внутри.

Как и концепты, диапазоны также перешли из экспериментального состояния в стандарт языка. Диапазоны зависят от концептов и используют их для улучшения обработки старого итератора, позволяя добавлять ограничения к обработанным значениям. Помимо типов, ограничивающих значения, диапазоны воспринимают представления (Views) как особую форму диапазона, позволяющую манипулировать данными или фильтровать их, возвращая изменённую версию данных исходного диапазона в качестве ещё одного диапазона. Например, есть вектор целых чисел, и необходимо получить все чётные значения в квадрате — диапазоны и представления помогут вам в этом.

Со всеми этими изменениями компилятор окажет гораздо большую помощь при проверке типов и представит более полезные сообщения об ошибках.

Форматирование строк

Если говорить про сообщения об ошибках и про вывод в целом, библиотека libfmt в соответствии с предложением её автора будет интегрирована в языковой стандарт как std::format. Это обеспечит функциональность форматирования строк как в Python. По сравнению со всей неловкостью метода cout и тем фактом, что использование printf() в контексте C++ — это нечто неправильное, такое дополнение будет очень желанным.

Форматирование в стиле Python предлагает почти те же функции, что и printf(), только с другим синтаксисом формата строки. Оно устраняет избыточность и предлагает некоторые полезные дополнения, такие как двоичное целочисленное представление и центрированный вывод с или без символов заполнения. Самое большое преимущество — это возможность определять правила форматирования для пользовательских типов. На первый взгляд такой метод похож на __str__() в Python или toString() в Java, но при этом он также добавляет собственные типы форматирования.

В качестве примера рассмотрим strftime(). Хоть это и функция C, которая ведёт себя как snprintf(), отличие состоит в том, что она определяет пользовательские конкретные символы преобразования для своей строки формата и ожидает struct tm в качестве аргумента. Правильно реализованный std::format может быть расширен, чтобы вести себя так же, как грядущее дополнение к библиотеке std::chrono.

Расположение

В C++ 20 появилась ещё одна экспериментальная функцияsource_location, обеспечивающая удобный доступ к имени файла, номеру строки или имени функции из текущего контекста вызова. В сочетании с std::format это основной кандидат для реализации пользовательской функции ведения журнала и практически современная альтернатива макросам препроцессора вроде __FILE__ и __LINE__.

Модули

Постепенное исключение препроцессора является долгосрочной целью в будущем C++ (consteval заменяет макрофункции, source_location заменяет один из наиболее часто используемых макросов). Модули — новый способ разделения исходного кода, который в конечном счёте призван заменить всю систему #include.

Пока одни разработчики говорят, что давно пора внести такие изменения, другие относятся к добавлению модулей достаточно критично​. Некоторые разработчики заявили о своей обеспокоенности по поводу их текущего состояния. Каково бы ни было ваше собственное мнение, можно с уверенностью сказать, что это серьёзное изменение всей сущности языка и в то же время достаточно сложное мероприятие, которое не произойдёт быстро. Если вы хотите познакомиться с модулями поближе, GCC и Clang уже в некоторой степени их поддерживают.

Многое другое

Это только малая часть всего списка, например Coroutines — ещё одна важная функция, которая будет добавлена в C++ 20. Но чтобы описать все изменения, потребуется ещё хотя бы одна статья. Поэтому просто перечислим их:

В целом C++ ждёт много изменений, и некоторые функции заслуживают того, чтобы ими восхищаться. Некоторые из этих новых функций и расширений были на других языках целую вечность (если не с самого начала). Поэтому интересно наблюдать, как некоторые из этих языков, которые когда-то находились под влиянием C++, теперь сами влияют на будущее этого языка.

Следите за новыми постами
Следите за новыми постами по любимым темам
25К открытий25К показов