Featured image for: Report from the virtual ISO C++ meetings in 2020 (core language).

The GNU Compiler Collection (GCC) 10.1 was released in May 2020. Like every other GCC release, this version brought many additions, improvements, bug fixes, and new features. Fedora 32 already ships GCC 10 as the system compiler, but it's also possible to try GCC 10 on other platforms (see godbolt.org, for example). Red Hat Enterprise Linux (RHEL) users will get GCC 10 in the Red Hat Developer Toolset (RHEL 7), or the Red Hat GCC Toolset (RHEL 8).

This article focuses on the part of the GCC compiler on which I spend most of my time: The C++ front end. My goal is to present new features that might be of interest to C++ application programmers. Note that I do not discuss developments in the C++ language itself, although some language updates overlap with compiler updates. I also do not discuss changes in the standard C++ library that comes with GCC 10.

We implemented many C++20 proposals in GCC 10. For the sake of brevity, I won't describe them in great detail. The default dialect in GCC 10 is -std=gnu++14; to enable C++20 features, use the -std=c++20 or -std=gnu++20 command-line option. (Note that the latter option allows GNU extensions.)

C++ concepts

While previous versions of GCC (GCC 6 was the first) had initial implementations of C++ concepts, GCC 10 updated concepts to conform to the C++20 specification. This update also improved compile times. Subsequent patches have improved concepts-related diagnostics.

In C++ template parameters, typename means any type. But most templates must be constrained in some way; as an example, you want to only accept types that have certain properties, not just any type. Failing to use the correct type often results in awful and verbose error messages. In C++20, you can constrain a type by using a concept, which is a compile-time predicate that specifies the set of operations that can be applied to the type. Using GCC 10, you can define your own concept (or use one defined in <concepts>):

#include <type_traits>
// Require that T be an integral type.
template<typename T> concept C = std::is_integral_v<T>;

And then use it like this:

template<C T> void f(T) { }
void g ()
{
  f (1); // OK
  f (1.2); // error: use of function with unsatisfied constraints
}

Starting with GCC 10, the C++ compiler also supports constrained auto. Using our concept above, you can now write:

int fn1 ();
double fn2 ();

void h ()
{
  C auto x1 = fn1 (); // OK
  C auto x2 = fn2 (); // error: deduced initializer does not satisfy placeholder constraints
}

Coroutines

GCC 10 supports stackless functions that can be suspended and resumed later without losing their state. This feature lets us execute sequential code asynchronously. It requires the -fcoroutines command-line option.

Unevaluated inline-assembly in constexpr functions

Code like this now compiles:

constexpr int
foo (int a, int b)
{
  if (std::is_constant_evaluated ())
    return a + b;
  // Not in a constexpr context.
  asm ("/* assembly */");
  return a;
}

See the proposal for details.

Comma expression in array subscript expressions

This type of expression is now deprecated, so GCC 10 warns for code like this:

int f (int arr[], int a, int b)
{
  return arr[a, b];
}

Only a top-level comma is deprecated, however, so arr[(a, b)] compiles without a warning.

Structured bindings

GCC 10 improves and extends structured bindings. For instance, it's now possible to mark them static:

struct S { int a, b, c; } s;
static auto [ x, y, z ] = s;

This example doesn't compile with GCC 9, but it compiles with GCC 10.

The constinit keyword

GCC 10 uses the C++20 specifier constinit to ensure that a (static storage duration) variable is initialized by a constant initializer.

This might alleviate problems with the static initialization order fiasco. However, the variable is not constant: It is possible to modify it after initialization has taken place. Consider:

constexpr int fn1 () { return 42; }
int fn2 () { return -1; }
constinit int i1 = fn1 (); // OK
constinit int i2 = fn2 (); // error: constinit variable does not have a constant initializer

GCC doesn't support Clang's require_constant_initialization attribute, so you can use __constinit in earlier C++ modes, as an extension, to get a similar effect.

Deprecated uses of volatile

Expressions that involve both loads and stores of a volatile lvalue, such as ++ or +=, are deprecated. The volatile-qualified parameter and return types are also deprecated, so GCC 10 will warn in:

void fn ()
{
  volatile int v = 42;
  // Load + store or just a load?
  ++v; // warning: deprecated
}

Conversions to arrays of unknown bound

Converting to an array of unknown bound is now permitted, so the following code compiles:

void f(int(&)[]);
int arr[1];
void g() { f(arr); }
int(&r)[] = arr;

Note: Conversion in the other direction—from arrays of unknown bound—currently is not allowed by the C++ standard.

constexpr new

This feature allows dynamic memory allocation at compile time in a constexpr context:

constexpr auto fn ()
{
  int *p = new int{10};
  // ... use p ...
  delete p;
  return 0;
}

int main ()
{
  constexpr auto i = fn ();
}

Note that the storage allocated at compile time in a constexpr context must also be freed at compile time. And, given that constexpr doesn't allow undefined behavior, use-after-free is a compile-time error. The new expression also can't throw. This feature paves the way for constexpr standard containers such as <vector> and <string>.

The [[nodiscard]] attribute

The [[nodiscard]] attribute now supports an optional argument, like so:

[[nodiscard("unsafe")]] int *fn ();

See the proposal for details.

CTAD extensions

C++20 class template argument deduction (CTAD) now works for alias templates and aggregates, too:

template <typename T>
struct Aggr { T x; };
Aggr a = { 1 }; // works in GCC 10

template <typename T>
struct NonAggr { NonAggr(); T x; };
NonAggr n = { 1 }; // error: deduction fails

Parenthesized initialization of aggregates

You can now initialize an aggregate using a parenthesized list of values such as (1, 2, 3). The behavior is similar to {1, 2, 3}, but in parenthesized initialization, the following exceptions apply:

  • Narrowing conversions are permitted.
  • Designators (things like .a = 10) are not permitted.
  • A temporary object bound to a reference does not have its lifetime extended.
  • There is no brace elision.

Here's an example:

struct A { int a, b; };
A a1{1, 2};
A a2(1, 2); // OK in GCC 10 -std=c++20
auto a3 = new A(1, 2); // OK in GCC 10 -std=c++20

Trivial default initialization in constexpr contexts

This usage is now allowed in C++20. As a result, a constexpr constructor doesn't necessarily have to initialize all the fields (but reading an uninitialized object is, of course, still forbidden):

struct S {
  int i;
  int u;
  constexpr S() : i{1} { }
};

constexpr S s; // error: refers to an incompletely initialized variable
S s2; // OK

constexpr int fn (int n)
{
  int a;
  a = 5;
  return a + n;
}

constexpr dynamic_cast

In constexpr contexts, you can now evaluate a dynamic_cast at compile time. Virtual function calls in constant expressions were already permitted, so this proposal made it valid to use a constexpr dynamic_cast, like this:

struct B { virtual void baz () {} };
struct D : B { };
constexpr D d;
constexpr B *b = const_cast<D*>(&d);
static_assert(dynamic_cast<D*>(b) == &d);

Previously, a constexpr dynamic_cast would have required a runtime call to a function defined by the C++ runtime library. Similarly, a polymorphic typeid is now also allowed in constexpr contexts.

Note: C++20 modules are not supported in GCC 10; they are still a work in progress (here is a related proposal). We hope to include them in GCC 11.

Additional updates

In non-C++20 news, the C++ compiler now detects modifying constant objects in constexpr evaluation, which is undefined behavior:

constexpr int
fn ()
{
  const int i = 5;
  const_cast<int &>(i) = 10; // error: modifying a const object
  return i;
}
constexpr int i = fn ();

GCC also handles the case when a constant object under construction is being modified and doesn't emit an error in that case.

Narrowing conversions

Narrowing conversions are invalid in certain contexts, such as list initialization:

int i{1.2};

GCC 10 is able to detect narrowing in more contexts where it's invalid, for instance, case values in a switch statement:

void g(int i)
{
  switch (i)
  case __INT_MAX__ + 1u:;
}

The noexcept specifier

GCC 10 properly treats the noexcept specifier as a complete-class context. As with member function bodies, default arguments, and non-static data member initializers, you can declare names used in a member function's noexcept specifier later in the class body. The following valid C++ code could not be compiled with GCC 9, but it compiles with GCC 10:

struct S {
  void foo() noexcept(b);
  static constexpr auto b = true;
};

The deprecated attribute

You can now use the deprecated attribute on namespaces:

namespace v0 [[deprecated("oh no")]] { int fn (); }

void g ()
{
  int x = v0::fn (); // warning: v0 is deprecated
}

GCC 9 compiles this code but ignores the attribute, whereas GCC 10 correctly warns about using entities from the deprecated namespace.

Defect report resolutions

We resolved several defect reports (DRs) in GCC 10. One example is DR 1710, which says that when we are naming a type, the template keyword is optional. Therefore, the following test compiles without errors with GCC 10:

template<typename T> struct S {
  void fn(typename T::template B<int>::template C<int>);
  void fn2(typename T::B<int>::template C<int>);
  void fn3(typename T::template B<int>::C<int>);
  void fn4(typename T::B<int>::C<int>);
};

Another interesting DR is DR 1307, which clarified how overload resolution ought to behave when it comes to choosing a better candidate based on the size-of-array initializer list. Consider the following test:

void f(int const(&)[2]);
void f(int const(&)[3]) = delete;

void g()
{
  f({1, 2});
}

GCC 9 rejects this test because it can't decide which candidate is better. Yet it seems obvious that the first candidate is best (never mind the = delete part; deleted functions participate in overload resolution). GCC 10 chooses this option, which lets the code compile.

Note: You can find the overall defect resolution status on the C++ Defect Report Support in GCC page.

Conclusion

In GCC 11, we plan to finish up the remaining C++20 features. For progress so far, see the C++2a Language Features table on the C++ Standards Support in GCC page. GCC 11 will also switch the default dialect to C++17 (it has already happened). Please do not hesitate to file bugs in the meantime, and help us make GCC even better!

Acknowledgments

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.

Last updated: February 5, 2024