Смягчение уязвимости Trojan Source, оптимизация функций приведения типов, многомерный оператор []
, подавление предупреждений о вендорных атрибутах — вот лишь некоторые возможности GCC 12. Подробностями делимся к старту курса по разработке на C++.
Релиз GNU GCC 12.1 (полный список изменений здесь) состоится в апреле 2022 года. Уже сейчас GCC 12 — системный компилятор в Fedora 36, и он будет доступен в Red Hat Enterprise Linux в Red Hat Developer Toolset 7 или Red Hat GCC Toolset 8 и 9. Я расскажу только о новых фичах, влияющих на C++.
Диалект GCC 12 по умолчанию — -std=gnu++17. C++23 указывается параметром -std=c++23; флаг -std=gnu++23 включает расширения GNU. C++20 и C++23 в GCC 12 по-прежнему экспериментальные, а в апрельском GCC реализовано ещё несколько предложений из С++23.
if consteval
В C++17 появился оператор constexpr if, условие в котором должно быть выражением-константой, (оно явно вычисляется как константа).
Если условие истинно, ветвь else отбрасывается, то есть не генерируется во время компиляции.
Если условие ложно, соответствующая ветвь также отбрасывается.
Если функция объявлена как constexpr, во время компиляции она может вычисляться или не вычисляться в зависимости от контекста.
Чтобы разработчик имел представление о том, когда вычисляется функция, в стандартную библиотеку C++20 введена функция std::is_constant_evaluated. Она возвращает true, когда текущий контекст вычисляется во время компиляции:
#include <type_traits>
int slow (int);
constexpr int fn (int n)
{
if (std::is_constant_evaluated ())
return n << 1; // #1
else
return slow (n); // #2
}
constexpr int i = fn (10); // does #1
int n = 10;
int i2 = fn (n); // calls slow function #2
В C++20 появилось ключевое слово consteval. Функция, функция-член или конструктор с consteval — это немедленная функция (immediate function), для которой компилятор не генерирует код.
Немедленные функции вычисляются во время компиляции и должны генерировать константу, кроме случаев, когда такая функция вызывается из другой немедленной функции, а если это не так, то возникает ошибка компиляции.
Как бы то ни было, в предыдущем тесте правила языка запрещают заменить n << 1 вызовом функции consteval:
#include <type_traits>
int slow (int);
consteval int fast (int n) { return n << 1; }
constexpr int fn (int n)
{
if (std::is_constant_evaluated ())
return fast (n); // 'n' is not a constant expression
else
return slow (n);
}
constexpr int i = fn (10);
Этот недочёт исправлен предложением о выражении if consteval, реализованном в GCC 12. С помощью if consteval вызывать немедленные функции можно так:
#include <type_traits>
int slow (int);
consteval int fast (int n) { return n << 1; }
constexpr int fn (int n)
{
if consteval {
return fast (n); // OK
} else {
return slow (n);
}
}
constexpr int i = fn (10);
if consteval допустимо в обычной функции, где нет constexpr. Кроме того, в отличие от обычного if, в if consteval фигурные скобки обязательны.
Проблема взаимодействия if constexpr и std::is_constant_evaluated, к счастью, обнаруживается компилятором, смотрите раздел std::is_constant_evaluated в предупреждениях для if.
auto(x)
GCC 12 допускает ключевое слово auto в приведении типов функционального стиля: auto приводится к prvalue.
struct A {};
void f(A&); // #1
void f(A&&); // #2
A& g();
void
h()
{
f(g()); // calls #1
f(auto(g())); // calls #2 with a temporary object
}
Обратите внимание: допустимы auto(x) и auto{x}, но не decltype(auto)(x).
Нелитеральные переменные в функциях constexpr
Когда функции constexpr не вычисляются как константы, GCC 12 допускает нелитеральные переменные, переходы goto и метки в них, что полезно для кода, подобного следующему (взят из предложения):
#include <type_traits>
template<typename T> constexpr bool f() {
if (std::is_constant_evaluated()) {
return true;
} else {
T t; // OK when T=nonliteral in C++23
return true;
}
}
struct nonliteral { nonliteral(); };
static_assert(f<nonliteral>());
Такой код не компилируется в C++20, но компилируется в C++23, потому что ветвь else не вычисляется. Также только в C++23 компилируется следующий пример:
constexpr int
foo (int i)
{
if (i == 0)
return 42;
static int a;
thread_local int t;
goto label;
label:
return 0;
}
Многомерный оператор []
В C++ функция-член operator[]
используется для доступа к элементам массива и элементам типов, подобных массивам, таким как std::array, std::span, std::vector и std::string. GCC 12 поддерживает многомерный оператор индекса. Выражения с запятыми в []
устарели в C++20, а в C++23 значение запятой в []
изменилось.
В C++20 оператор []
принимал только один элемент, поэтому доступ к элементам многомерных массивов получали через операторы вызова функций: arr(x, y, z), и похожих обходных путей, не лишённых недостатков. Поэтому C++23 позволяет оператору []
принимать ноль или более аргументов, а значит, при компиляции с флагом -std=c++23 будет работать этот пример:
template <typename... T>
struct W {
constexpr auto operator[](T&&...);
};
W<> w1;
W<int> w2;
W<int, int> w3;
Понятнее будет этот код с крайне наивной реализацией:
struct S {
int a[64];
constexpr S () : a {} {};
constexpr S (int x, int y, int z) : a {x, y, z} {};
constexpr int &operator[] () { return a[0]; }
constexpr int &operator[] (int x) { return a[x]; }
constexpr int &operator[] (int x, long y) { return a[x + y * 8]; }
};
void g ()
{
S s;
s[] = 42;
s[5] = 36;
s[3, 4] = 72;
}
В качестве расширения GNU, если GCC не находит перегрузку оператора []
, то ведёт себя как раньше, но выдаёт предупреждение:
void f(int a[], int b, int c)
{
a[b,c]; // deprecated in C++20, invalid but accepted with a warning in C++23
a[(b,c)]; // OK in both C++20 and C++23
}
Учтите, что operator[]
не поддерживает аргументы по умолчанию, но представляется, что их разрешат позже. Если и когда это случится, будет работать код ниже:
struct X {
int a[64];
constexpr int& operator[](int i = 1) { return a[i]; }
};
elifdef и elifndef
В C и C++ директивы #ifdef и #ifndef — это синтаксический сахар для #ifdefined(...) и #if !defined(...). Удивительно, но у других вариантов этих директив таких сокращений нет. Чтобы исправить это, разработчики C и C++ приняли предложения N2645 и P2334R1. В GCC 12 реализованы оба предложения, поэтому следующий пример компилируется:
#ifdef __STDC__
/* ... */
#elifndef __cplusplus
#warning "not ISO C"
#else
/* ... */
#endif
Для компиляции этого примера в C++20 и более ранних версиях нужно включить расширения GNU. Другими словами, флаг -std=c++20 приводит к ошибке компиляции, а с флагами -std=gnu++20 и -Wpedantic — к предупреждению типа pedantic, то есть о несоответствии стандарту ISO.
Псевдонимы в операторе инициализации
GCC 12 разрешает объявлять псевдонимы в операторе инициализации, используемом внутри if, for и switch:
for (using T = int; T e : v)
{
// use e
}
Исправления и внутренние улучшения
Изменения в этом разделе согласуют GCC с нововведениями стандарта и разрешают ранее некорректное поведение.
Изменения поиска зависимого оператора
До GCC 12 компилятор выполнял неквалифицированный поиск зависимого операторного выражения во время определения шаблона, а не во время инстанцирования. Поведение GCC 12 соответствует существующему поведению зависимых выражений вызова:
#include <iostream>
namespace N {
struct A { };
}
void operator+(N::A, double) {
std::cout << "#1 ";
}
template<class T>
void f(T t) {
operator+(t, 0);
t + 0;
}
// Since it's not visible from the template definition, this later-declared
// operator overload should not be considered when instantiating f<N::A>(N::A),
// for either the call or operator expression.
void operator+(N::A, int) {
std::cout << "#2 ";
}
int main() {
N::A a;
f(a);
std::cout << std::endl;
}
В GCC 11 и старых компиляторах программа выведет #1 #2, но в GCC 12 — #1 #1: ранее в перегрузку разрешалось только выражение вызова #1, а в GCC 12 в перегрузку разрешается и операторное выражение.
Спецификатор auto для указателей и ссылок на массивы
GCC 12 поддерживает предложение из отчёта о дефектах DR2397: спецификатор auto для указателей и ссылок на массивы, и теперь тип элемента массива может быть типом со спецификатором auto, то есть код ниже работает:
int a[3];
auto (*p)[3] = &a;
auto (&r)[3] = a;
Но массив неявно преобразуется в указатель, поскольку вывод типа auto здесь выполняется с точки зрения вывода аргумента шаблона функции, поэтому не работает ни один из фрагментов ниже:
auto (&&r)[2] = { 1, 2 };
auto arr[2] = { 1, 2 };
int arr[5];
auto x[5] = arr;
Хотя когда-нибудь он может заработать.
Свёртывание тривиальных функций
std::move или std::forward — это приведения типов, реализованные как вызовы функций, то есть компилятор генерирует отладочную информацию, сохраняемую даже после встроенного вызова, хотя отлаживать такой код не нужно.
GCC 12 преобразует вызовы некоторых тривиальных встроенных функций, например std::move, std::forward, std::addressof и std::as_const) в простые приведения типов. Это часть общей процедуры свёртывания выражений при оптимизации компилятором.
Так количество отладочной информации GCC может сократиться на 10%, также сокращаются используемая память и время компиляции. Оптимизация включается флагом -ffold-simple-inlines.
Ограничения в излишне свободной спецификации enum direct-list-initialization
GCC 12 учитывает отчёт о дефекте, предложение в котором запрещает, например, прямую инициализацию перечисления с ограниченной областью действия из другого перечисления этого же типа:
enum class Orange;
enum class Apple;
Orange o;
Apple a{o}; // error with GCC 12
Нетиповые аргументы шаблона в частичных специализациях
Ранее использование некоторых параметров шаблона как его аргументов запрещалось. Этот запрет сняли в ответ на отчёт о дефекте. Следующее вероятное применение параметра шаблона как его аргумента скомпилируется:
template <int I, int J> struct A {};
template <int I> struct A<I, I*2> {}; // OK with GCC 12
Подстановки в параметры функции в лексическом порядке
Вывод аргументов шаблона C++ несколько изменился, когда в отчёте указали, что подстановка выполняется, слева направо, то есть в лексическом порядке, из-за чего может возникнуть нежелательный эффект:
template <typename T>
struct A { using type = typename T::type; };
template <typename T> void g(T, typename A<T>::type);
template <typename T> long g(...);
long y = g<void>(0, 0); // OK in GCC 12, error in GCC 11
template <class T> void h(typename A<T>::type, T);
template <class T> long h(...);
long z = h<void>(0, 0); // error in GCC 12, OK in GCC 11
GCC 12 подставляет аргументы слева направо, проверяя ошибку типа до подстановки оставшихся аргументов. Таким образом, для g<void>(0, 0) компилятор пытается подставить void в g(T, typename A<T>::type) и видит, что первая замена приводит к недопустимому для параметра типу void.
Эта замена — SFINAE («неудачная подстановка — не ошибка»), поэтому вместо первой перегрузки, которая отбрасывается, выбирается g(...). Но для h<void>(0, 0) компилятор в typename A<T>::type сначала подставляет void. И это серьёзная ошибка, ведь создание экземпляра A не является непосредственным контекстом.
GCC 11 и более ранние версии выполняли эту замену справа налево, приводя к обратному: g<void>(0, 0) вызывало ошибку компиляции, тогда как h<void>(0, 0) компилировалось.
Более строгая проверка атрибутов в объявлениях friend
Объявление friend с любым атрибутом должно быть определением, но до версии 12 GCC не проверял это соответствие. Кроме того, атрибут C++11 не может находиться в середине decl-specifier-seq:
template<typename T>
struct S {
[[deprecated]] friend T; // warning: attribute ignored
[[deprecated]] friend void f(); // warning: attribute ignored
friend [[deprecated]] int f2(); // error
};
S<int> s;
Дедуктивные руководства (deduction guides) теперь могут объявляться в области видимости класса
Из-за бага в версиях GCC до 11 включительно дедуктивные руководства нельзя было объявить на уровне класса. Ошибка исправлена в GCC 12, следующий код компилируется:
struct X {
template<typename T> struct A {};
A() -> A<int>;
};
GCC 12 также поддерживает руководства по дедукциям без шаблонов на уровне класса.
Cравнение нулевых указателей теперь отклоняется
Сравнение типа больше-меньше между константами нулевого указателя и указателями имеют неправильный формат, что диагностируется в GCC 12:
decltype(nullptr) foo ();
auto cmp = foo () > 0; // error: ordered comparison of pointer with integer zero
Общий статус устранения дефектов указан на странице C++ Defect Report Support in GCC.
Новые и доработанные предупреждения
В GCC 12 доработали множество предупреждений:
-Wuninitialized
Теперь -Wuninitialized предупреждает об использовании неинициализированных переменных в списках инициализаторов элементов, а значит, оптимизирующий компилятор может обнаруживать такие ошибки:
struct A {
int a;
int b;
A() : b(1), a(b) { }
};
Здесь поле b используется неинициализированным: порядок инициализаторов элементов в списке инициализаторов не важен, но важен порядок объявлений в определении класса. Для предупреждения, когда порядок инициализаторов элементов не соответствует порядку объявления, можно использовать -Wreorder.
-Wuninitialized не предупреждает о более сложных инициализаторах и об использовании адреса объекта:
struct B {
int &r;
int *p;
int a;
B() : r(a), p(&a), a(1) { } // no warning
};
Кстати, запрос на усиление этого предупреждения поступил около 17 лет назад. Видимо, иногда что-то требует времени.
-Wbidi-chars
-Wbidi-chars должно смягчить CVE-2021-42574, известную как Trojan Source. Оно предупреждает о двунаправленных управляющих символах UTF-8, которые могут изменить направление письма слева направо на справа налево и наоборот.
Некоторые комбинации управляющих символов могут запутать программиста: код, который кажется закомментированным, на самом деле компилируется, или наоборот. Чтобы узнать больше, смотрите статью Дэвида Малкольма Prevent Trojan Source attacks with GCC 12 на Red Hat Developer.
-Warray-compare
Новая опция -Warray-compare предупреждает об устаревшем в C++20 способе сравнения массивов:
int arr1[5];
int arr2[5];
bool same = arr1 == arr2; // warning: comparison between two arrays
-Wattributes
Опции -Wno-attributes=ns::attr и -Wno-attributes=ns:: подавляют предупреждения о неизвестных атрибутах области действия в C++11 и C2X. Также для этого можно использовать #pragma GCC Diagnostic ignored_attributes "ns::attr".
Новое поведение обнаружит опечатки и устранит нежелательные предупреждения о вендорных атрибутах:
[[deprecate]] void g(); // warning: should be deprecated
[[company::attr]] void f(); // no warning
При компиляции с флагом -Wno-attributes=company:: предупреждение вызовет только первое объявление.
Новые параметры предупреждений о несоответствии версий языка
По умолчанию в GCC 12 включены эти параметры предупреждений:
-Wc++11-extensions
-Wc++14-extensions
-Wc++17-extensions
-Wc++20-extensions
-Wc++23-extensions
Эти параметры управляют существующими предупреждениями pedantic о появлении новых конструкций C++ в коде по старому стандарту. Например, новым параметром -Wno-c++11-extensions можно подавлять предупреждения об использовании вариативных шаблонов в коде C++98.
std::is_constant_evaluated в предупреждениях для if
Условие в if constexpr явно вычисляются как константа, а значит, if constexpr (std::is_constant_evaluated()) — всегда true. Предупреждение появилось в GCC 10, GCC 12 распространил его на сомнительные случаи:
#include <type_traits>
int
foo ()
{
if (std::is_constant_evaluated ()) // warning: always evaluates to false in a non-constexpr function
return 1;
return 0;
}
consteval int
baz ()
{
if (std::is_constant_evaluated ()) // warning: always evaluates to true in a consteval function
return 1;
return 0;
}
-Wmissing-requires
Опция -Wmissing-requires предупреждает о пропущенных requires:
template <typename T> concept Foo = __is_same(T, int);
template<typename Seq>
concept Sequence = requires (Seq s) {
/* requires */ Foo<Seq>;
};
Разработчик, видимо, хотел вызвать концепт Foo (вложенное requires), перед которым должно стоять ключевое слово requires. В этом тесте Foo — это идентификатор-концепт, который делает Sequence истинным, если выражение Foo валидное. Выражение Foo справедливо для всех Seq.
-Waddress
-Waddress теперь предупреждает, например, о сравнении адреса нестатической функции-члена с nullptr:
struct S {
void f();
};
int g()
{
if (&S::f == nullptr) // warning: the address &S::f will never be NULL
return -1;
return 0;
}
Благодарности
Как и всегда, я хотел бы поблагодарить своих коллег из Red Hat, которые сделали компилятор GNU C++ намного лучше, особенно Джейсона Меррилла, Якуба Елинека, Патрика Палку и Джонатана Уэйкли.
Заключение
В GCC 13 мы планируем доработать оставшиеся особенности C++23, см. в таблицу C++23 Language Features на странице C++ Standards Support in GCС. Отправляйте отчёты об ошибках и не стесняйтесь помогать делать GCC ещё лучше.
А мы поможем прокачать ваши навыки или с самого начала освоить профессию, востребованную в любое время:
Выбрать другую востребованную профессию.
Краткий каталог курсов и профессий
Data Science и Machine Learning
Python, веб-разработка
Мобильная разработка
Java и C#
От основ — в глубину
А также