Featured image for: Value range propagation in GCC with Project Ranger.

Version 12.1 of the GNU Compiler Collection (GCC) is expected to be released in April 2022. Like every major GCC release, this version will bring many additions, improvements, bug fixes, and new features. GCC 12 is already the system compiler in Fedora 36. GCC 12 will also be available on Red Hat Enterprise Linux in the Red Hat Developer Toolset (version 7) or the Red Hat GCC Toolset (version 8 and 9).

Like the article I wrote about GCC 10, this article describes only new features affecting C++.

We implemented several C++23 proposals in GCC 12. The default dialect in GCC 12 is -std=gnu++17; to enable C++23 features, use the -std=c++23 or -std=gnu++23 command-line options. (The latter option allows GNU extensions.)

Note that C++20 and C++23 features are still experimental in GCC 12.

C++23 features

A number of previously prohibited constructs are now allowed, and some of these features can potentially reduce the size of your programs.

if consteval

C++17 introduced the constexpr if statement. The condition in if constexpr must be a constant expression (it is manifestly constant evaluated). If the condition evaluates to true, the else branch, if present, is discarded. Discarded means that the else branch is not instantiated at all during compilation, which is different behavior from an ordinary if. If the condition evaluates to false, the then branch is similarly discarded.

If a function is declared constexpr, it might or might not be evaluated at compile time, depending on the context. To offer some visibility to the programmer about when the function is evaluated at compile time, C++20 introduced a new library function, std::is_constant_evaluated, which returns true if the current context is evaluated at compile time:

#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 introduced the consteval keyword. A (possibly member) function or a constructor marked as consteval is an immediate function. Immediate functions are evaluated during compilation and must produce a constant, unless the call to an immediate function takes place in another immediate function; if they don't, the compiler produces an error. The compiler doesn't emit any actual code for such functions.

However, the language rules do not allow the developer to replace n << 1 in the preceding test with a call to a consteval function:

#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);

To fix this problem, proposal P1938R3 introduced if consteval, which GCC 12 implements. if consteval allows the developer to invoke immediate functions, as shown here:

#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);

Note that it is valid to have if consteval in an ordinary, non-constexpr function. Also note that if consteval requires { }, unlike the ordinary if statement.

There is a problem with the interaction between if constexpr and std::is_constant_evaluated, but fortunately the compiler can detect that problem. We'll examine the solution in the later section Extended std::is_constant_evaluated in if warning.

auto(x)

GCC 12 implements proposal P0849, which allows auto in a function-style cast, the result of which is a pure rvalue (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
}

Note that both auto(x) and auto{x} are accepted; however, decltype(auto)(x) remains invalid.

Non-literal variables in constexpr functions

GCC 12 implements C++23 proposal P2242R3, which allows non-literal variables, gotos, and labels in constexpr functions so long as they are not constant evaluated. This expanded behavior is useful for code like the following (taken from the proposal):

#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>());

This does not compile in C++20, but compiles in C++23 because the else branch is not evaluated. The following example also compiles only in C++23:

constexpr int
foo (int i)
{
  if (i == 0)
    return 42;
  static int a;
  thread_local int t;
  goto label;
label:
  return 0;
}

Multidimensional subscript operator

GCC 12 supports C++23 proposal P2128R6, a multidimensional subscript operator. Comma expressions within subscripting expressions were deprecated in C++20 via proposal P1161R3, and in C++23 the comma in [ ] has changed meaning.

C++ uses the operator[] member function to access the elements of an array, as well as array-like types such as std::array, std::span, std::vector, and std::string. However, this operator did not accept multiple arguments in C++20, so access to the elements of multidimensional arrays was implemented using function call operators like arr(x, y, z), and similar workarounds. These workarounds have a number of drawbacks, so to alleviate the issues when using them, C++23 allows operator[] to take zero or more arguments.

As a consequence, this test case is accepted with -std=c++23:

template <typename... T>
struct W {
  constexpr auto operator[](T&&...);
};

W<> w1;
W<int> w2;
W<int, int> w3;

Here is what may be a clearer example, with a very naive implementation:

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;
}

As an extension, GCC still supports the old behavior when an overloaded subscript operator is not found, though it issues a warning:

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
 }

Note that currently, operator[] does not support default arguments. It appears, though, that default arguments will be allowed in future versions: see CWG 2507. If and when the proposed adjustment is accepted, the following example will be allowed:

struct X {
  int a[64];
  constexpr int& operator[](int i = 1) { return a[i]; }
};

elifdef and elifndef

In C and C++, the #ifdef and #ifndef preprocessing directives are "syntactic sugar" for #if defined(something) and #if !defined(something). Surprisingly, the else variants of these directives did not have the same shorthands. To amend this omission, the C and C++ designers accepted proposals N2645 and P2334R1, respectively. GCC 12 implements both proposals, so the following example compiles correctly:

#ifdef __STDC__
/* ... */
#elifndef __cplusplus
#warning "not ISO C"
#else
/* ... */
#endif

Please note that, to compile this example without errors in C++20 and earlier, you must enable GNU extensions. In other words, -std=c++20 causes a compile error, but -std=gnu++20 causes only a pedantic warning if -Wpedantic is also turned on.

Extended init-statement

GCC 12 implements proposal P2360R0 in C++23, which merely extends an init-statement (used in if, for, and switch statements) to allow it to contain an alias-declaration. In practice, that change means that the following code is accepted:

for (using T = int; T e : v)
  {
    // use e
  }

Corrections and internal improvements

The changes described in this section bring GCC more in line with recent changes to the standard, and permit behavior that previously did not work correctly.

Dependent operator lookup changes

GCC 12 corrected a problem where the compiler performed an unqualified lookup for a dependent operator expression at template definition time instead of at instantiation time. The fix matches the existing behavior for dependent call expressions. Consider the following test case demonstrating this change:

#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;
}

This program will print #1 #2 when compiled with versions 11 or older of GCC, but GCC 12 correctly prints #1 #1. That's because previously only the call expression resolved to the #1 overload, but with GCC 12 the operator expression does too.

auto specifier for pointers and references to arrays

GCC 12 also supports defect report DR2397, covering the auto specifier for pointers and references to arrays. This change removes the restriction that the array element type may not be a placeholder type. This allows code like:

int a[3];
auto (*p)[3] = &a;
auto (&r)[3] = a;

However, neither of the following examples work (although some day they might):

auto (&&r)[2] = { 1, 2 };
auto arr[2] = { 1, 2 };

int arr[5];
auto x[5] = arr;

These don't work because the auto deduction is performed in terms of function template argument deduction, so the array decays to a pointer.

Folding of trivial functions

A well-formed call to std::move or std::forward is equivalent to a cast. But because these constructs are implemented as function calls, the compiler generates debugging information that persists even after the call gets inlined. This extra code is a waste because there's no need to debug such operations. Therefore, GCC 12 elides calls to certain trivial inline functions (such as std::move, std::forward, std::addressof, and std::as_const) into simple casts as part of the front end's general expression folding routine. As a result, the debugging information produced by GCC could be up to 10 percent smaller, while improving GCC's compile time and memory usage. This behavior is controlled by a new option called -ffold-simple-inlines.

Fixing the overly permissive specification of enum direct-list-initialization

GCC 12 implements defect report DR2374, which forbids, for instance, direct-list-initialization of a scoped enumeration from a different scoped enumeration:

enum class Orange;
enum class Apple;
Orange o;
Apple a{o}; // error with GCC 12

Restrictions on non-type template arguments in partial specializations

Previously, an overly strict restriction prevented certain uses of template parameters as template arguments. This restriction has been rectified in response to defect report DR1315, and GCC implements these uses now. Therefore, the following plausible use of a template parameter as a template argument compiles correctly:

template <int I, int J> struct A {};
template <int I> struct A<I, I*2> {}; // OK with GCC 12

Substitutions into function parameters in lexical order

C++ template argument deduction underwent some changes when defect report DR1227 specified that the substitution proceeds in lexical order—that is, from left to right. The following code demonstrates what effect this might have:

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 substitutes the arguments in left-to-right order and checks whether a substituted type is erroneous before substituting it into the rest of the arguments. Thus, for g<void>(0, 0) the compiler tries to substitute void into g(T, typename A<T>::type) and sees that the first substitution results in an invalid parameter type void. This invalid substitution is a SFINAE failure, so the first overload is discarded and the g(...) one is chosen instead. However, for h<void>(0, 0), the compiler first substitutes void into the typename A<T>::type parameter. This produces a hard error, because instantiating A<void> is not an immediate context.

GCC 11 and earlier performed the substitution in right-to-left order, so the situation was reversed: g<void>(0, 0) resulted in a compile error, whereas h<void>(0, 0) compiled fine.

Stricter checking of attributes on friend declarations

If a friend declaration has an attribute, that declaration must be a definition, but before version 12, GCC didn't check this restriction. Moreover, a C++11 attribute cannot appear in the middle of the 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 can be declared at class scope

Due to a bug, deduction guides could not be declared at class scope in versions 11 and early of GCC. This has been fixed in GCC 12, so the following test case compiles correctly:

struct X {
  template<typename T> struct A {};
  A() -> A<int>;
};

Class-scope non-template deduction guides are now supported as well in GCC 12.

Ordered comparison of null pointers is now rejected

Relational comparisons between null pointer constants and pointers are ill-formed, and this error is diagnosed in GCC 12:

decltype(nullptr) foo ();
auto cmp = foo () > 0; // error: ordered comparison of pointer with integer zero

The overall defect resolution status is listed in the C++ Defect Report Support in GCC page.

New and improved warnings

GCC's plethora of warning options have been enhanced in GCC 12.

-Wuninitialized extended

The -Wuninitialized warning has been extended to warn about using uninitialized variables in member initializer lists. Therefore, the front end can detect bugs like this:

struct A {
  int a;
  int b;
  A() : b(1), a(b) { }
};

Here, the b field is used uninitialized because the order of member initializers in the member initializer list is irrelevant; what matters is the order of declarations in the class definition. (A related warning, -Wreorder, can be used to warn when the order of member initializers does not match the declaration order.)

The warning does not warn about more complex initializers. And it also does not warn when the address of an object is used:

struct B {
  int &r;
  int *p;
  int a;
  B() : r(a), p(&a), a(1) { } // no warning
};

As an aside, the request to enhance this warning came about 17 years ago. Apparently, sometimes things take time.

-Wbidi-chars added

The -Wbidi-chars warning warns about potentially misleading UTF-8 bidirectional control characters, which can change left-to-right writing direction into right-to-left (and vice versa). Certain combinations of control characters might cause confusion for the programmer because code that has seemingly been commented out might actually be compiled, or vice versa. This warning is supposed to mitigate CVE-2021-42574, aka Trojan Source.

For more information, please refer to David Malcolm's Red Hat Developer article Prevent Trojan Source attacks with GCC 12.

-Warray-compare added

The new -Warray-compare option warns about comparisons between two operands of array type, which was deprecated in C++20. Here's an example of this situation:

int arr1[5];
int arr2[5];
bool same = arr1 == arr2; // warning: comparison between two arrays

-Wattributes extended

The -Wattributes warning has been extended so that developers can now use -Wno-attributes=ns::attr or -Wno-attributes=ns:: to suppress warnings about unknown scoped attributes (in C++11 and C2X). Similarly, #pragma GCC diagnostic ignored_attributes "ns::attr" can be used to achieve the same effect. The new behavior is meant to help with vendor-specific attributes, where a warning is not desirable, while still detecting typos. Consider:

[[deprecate]] void g(); // warning: should be deprecated
[[company::attr]] void f(); // no warning

When compiled with -Wno-attributes=company::, only the first declaration issues a warning.

New warning options for C++ language mismatches

GCC 12 gained the following new warning options, enabled by default:

  • -Wc++11-extensions
  • -Wc++14-extensions
  • -Wc++17-extensions
  • -Wc++20-extensions
  • -Wc++23-extensions

These options control existing pedantic warnings about occurrences of new C++ constructs in code that uses an older C++ standard dialect. For instance, developers are now able to suppress warnings when using variadic templates in C++98 code by applying the new -Wno-c++11-extensions option.

Extended std::is_constant_evaluated in if warning

Because the condition in if constexpr is manifestly constant evaluated, if constexpr (std::is_constant_evaluated()) is always evaluated to be true. GCC 10 introduced a warning to detect this bug, and GCC 12 extended the warning to detect more dubious cases. For instance:

#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 added

The -Wmissing-requires option warns about a missing requires. Consider the following code:

template <typename T> concept Foo = __is_same(T, int);

template<typename Seq>
concept Sequence = requires (Seq s) {
  /* requires */ Foo<Seq>;
};

The problem here is that the developer presumably meant to invoke the Foo concept (a nested requirement), which needs to be prefixed by the requires keyword. In this test, Foo<Seq> is a concept-id, which makes Sequence true if Foo<Seq> is a valid expression. The expression is valid for all Seq.

-Waddress enhanced

The -Waddress warning has been extended. It now warns about, for instance, comparing the address of a nonstatic member function to the null pointer value:

struct S {
  void f();
};

int g()
{
  if (&S::f == nullptr) // warning: the address &S::f will never be NULL
    return -1;
  return 0;
}

Acknowledgments

As usual, I'd like to thank my coworkers at Red Hat who made the GNU C++ compiler so much better, notably Jason Merrill, Jakub Jelinek, Patrick Palka, and Jonathan Wakely.

Conclusion

In GCC 13, we plan to finish up the remaining C++23 features. For progress so far, see the C++23 Language Features table on the C++ Standards Support in GCC page. Please do not hesitate to file bug reports in the meantime and help us make GCC even better.

Last updated: August 14, 2023