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

The GNU Compiler Collection (GCC), which is the standard compiler on GNU/Linux distributions such as Fedora and Red Hat Enterprise Linux, moved from version 14 to version 17 of C++ in April 2021. Thus, the -std=gnu++17 command-line option is now used by default.

C++17 brings a host of new features, but also deprecates, removes, or changes the semantics of certain constructs. This article looks at some of the issues you might face when switching to GCC 11. Remember that it is always possible to use the previous version of C++ by specifying the -std=gnu++14 option. Moreover, this article deals only with the core language; we won't discuss deprecated or removed features in the standard C++ library (such as auto_ptr). For a broader overview, I encourage visiting the paper Changes between C++14 and C++17. For more information regarding switching to using GCC 11, please see our upstream document, Porting to GCC 11.

Removed in C++17

We'll start with what has been removed in C++17: Trigraphs, the register keyword, and increments on the bool type.

Trigraphs

In C++, a trigraph is a sequence of three characters starting with ?? that can express single punctuation characters. For instance, \ can be written as ??/. The reason for this is historical: C and C++ use special characters such as [ and ] that are not defined in the ISO 646 character set (and so some keyboards are missing these keys). The positions of these characters in the ISO table might be occupied by different characters in national ISO 646 characters sets, such as ¥ in place of \.

Trigraphs were meant to allow programmers to enter the characters that weren't on the keyboard. But in practice, trigraphs are likely to be used only by accident, so they were removed in C++17. The removal allows you to play "cute" games like the following test. Can you see why it works?

bool cxx_with_trigraphs_p () {
  // Are we compiling in C++17??/
  return false;
  return true;
}

If for some reason you still need to use trigraphs in C++17 (indeed, there are code bases that still use trigraphs), GCC offers the -trigraphs command-line option.

The register keyword

The register keyword was deprecated in C++11 with CWG 809 because it had no practical effect. C++17 cleaned this up further by removing the keyword completely (though it remains reserved for future use). Therefore, code such as the following causes GCC 11 to warn by default, and to issue an error when the -pedantic-errors option is used:

void f () {
  register int i = 42; // warning: ISO C++17 does not allow 'register' storage class specifier
  int register; // error
}

In C++14, you can instruct the compiler to warn using -Wregister, which ought to help you migrate code to C++17. Note that GCC still accepts the GNU explicit register variables extension without warning:

register int g asm ("ebx");

Increments on the bool type

In C++, the -- operator has never been supported for objects of type bool. But ++ on a bool was originally valid, and was deprecated but not removed in C++98. In the spirit of C++17, post-increment and pre-increment are now forbidden, so GCC will give an error for code that uses them:

template<typename T>
void f() {
  bool b = false;
  b++; // error: use of an operand of type 'bool' in 'operator++' is forbidden in C++17
  T t{};
  t++; // error: use of an operand of type 'bool' in 'operator++' is forbidden in C++17
}

void g() {
  f<bool>();
}

Instead of ++, simply use b = true or b |= true depending on the specific case.

Exception specification changes

Keywords related to exceptions have been added to C++ over various versions of the language. C++17 introduces changes to the noexcept specification while removing dynamic exception specifications.

Exception specifications are now part of the type system

C++1 1 added the noexcept exception specification in N3050. Since C++17, noexcept has become part of the type. Thus, the following two functions have the same type in C++14, but different types in C++17:

int foo () noexcept;
int bar ();

However, this doesn't mean that noexcept is part of a function's signature. Therefore, you cannot overload a function as follows:

int baz();
int baz() noexcept; // error: different exception specifier (even in C++11)

Another consequence is that in C++17, a pointer to a function that could potentially throw cannot be converted to a function that cannot throw:

void (*p)();
void (*q)() noexcept = p; // OK in C++14, error in C++17

This change also affects template arguments. The following program will not compile in C++17, because the compiler deduces two conflicting types for the template parameter T:

void g1 () noexcept;
void g2 ();
template<typename T> int foo (T, T);
void f() {
  foo (g1, g2); // error: void (*)() noexcept vs void (*)()
}

Interestingly, this change was first discussed more than 20 years ago in CWG 92, and was finally adopted in P0012R1.

Removal of dynamic exception specifications

C++11 deprecated dynamic exception specifications in N3051, and C++17 removed them altogether in P0003R5. The exception is that throw() continues to work in C++17, and is equivalent to noexcept(true), although it has been removed in C++20. Moreover, in C++17, the function std::unexpected was removed. This function used to be called if a function decorated with throw() actually did throw an exception. In C++17, std::terminate is called instead.

C++17 code cannot use dynamic exception specifications and should replace throw() with noexcept.

The following example might help to clarify the usage:

void fn1() throw(int); // error in C++17, warning in C++14
void fn2() noexcept; // OK since C++11
void fn3() throw(); // deprecated but no warning in C++17
void fn4() throw() { throw; }
// In C++14, calls std::unexpected which calls std::unexpected_handler
// (which is std::terminate by default).
// In C++17, calls std::terminate directly.

New template template-parameter matching

The C++17 proposal P0522R0: Matching of template template-arguments excludes compatible templates, which fixed DR 150, was implemented in GCC 7 in the -fnew-ttp-matching option but was turned off by default. Because GCC 11 defaults to C++17, the new behavior is enabled by default.

In the old behavior, the template template-argument for a template template-parameter must be a template with a parameter list that exactly matches the corresponding parameters in the template parameter's parameter list. The new behavior is less strict, by considering default template arguments of template template-arguments in the match.

This change was problematic for some templates in the standard library. For instance, std::deque is a template with the following default template argument:

template<typename T, typename Allocator = std::allocator<T>>
class deque;

Therefore, the following code works only with the new GCC 11 behavior:

#include <deque>

template <template <typename> class>
void fn() {}
template void fn<std::deque>();

The workaround is to adjust the declaration so that the parameter expects two type parameters:

#include <deque>

template <template <typename, typename> class>
void fn() {}
template void fn<std::deque>();

However, the new behavior can also cause code that worked in the old behavior to stop compiling with an error:

template <int N, int M = N> class A { };
template <int N, int M> void fn(A<N, M>) {}
template <int N, template <int> typename T> void fn(T<N>);

void g ()
{
  A<3> a;
  fn (a); // ambiguous in C++17
}

The reason is that A is considered a valid argument for T in the new behavior. Therefore, both function templates are valid candidates, and because neither is more specialized than the other, the function call to fn has become ambiguous.

It's possible to revert to the old behavior, even in C++17 mode, by using -fno-new-ttp-matching.

Static constexpr and consteval class members implicitly inline

The C++17 proposal to introduce inline variables (P0386R2) brought the following change into the [dcl.constexpr] section of the specification: "A function or static data member declared with the constexpr or consteval specifier is implicitly an inline function or variable."

As a consequence, the member variable A::n in the following example is a definition in C++17. The declaration labeled #2 in a comment was required in C++14. In C++17, the #2 declaration can be removed:

struct A {
  static constexpr int n = 5; // #1, definition in C++17, declaration in C++14
};

constexpr int A::n; // #2, definition in C++14, deprecated redeclaration in C++17

auto g()
{
  return &A::n; // ODR-use of A::n -- needs a definition
}

Changes to evaluation order

C++17 P0145R3 clarified the order of evaluation of various expressions. As the proposal states, the following expressions are evaluated in such a way that a is evaluated before b:

  • a.b
  • a->b
  • a->*b
  • a (b1, b2, b3)
  • b op= a
  • a[b]
  • a << b
  • a >> b

These rules have caused some changes in the GCC compiler, and certain code might behave differently in C++14 and C++17, as the following test illustrates. It is possible to selectively adjust the compiler behavior using the -fstrong-eval-order={all|some|none} compile-time option, where all is the default in C++17 and some is the default in C++14.

int fn ()
{
  int ar[4]{ };
  int i = 0;
  ar[i++] = i;
  return ar[0]; // returns 0 in C++17, 1 in C++14
}

int fn2 ()
{
  int x = 2;
  return x << (x = 1, 2); // returns 8 in C++17, 4 in C++14
}

int fn3 ()
{
  int x = 6;
  return x >> (x = 5, 1); // returns 3 in C++17, 2 in C++14
}

The aforementioned P0145R3 paper also defines the evaluation order when some of the affected operands are overloaded and are using the operator syntax: They follow the order prescribed for the built-in operator.

Guaranteed copy elision

C++17 requires guaranteed copy elision, meaning that a copy or move constructor call will be elided completely under certain circumstances (such as when the type of the initializer and target are the same), even when the call has side effects. That means that, theoretically, if something relied on a constructor being instantiated, for example, via copying a function parameter, the program could now fail, because the constructor might not be instantiated in C++17.

GCC already performed copy/move elision as an optimization even in C++14 mode, so such failures are unlikely to happen in practice. However, the difference is that in C++17 the compiler will not perform access checking on the elided constructor, so code that didn't compile previously could compile now, as demonstrated by the following snippet:

class A {
  int i;
public:
  A() : i{42} {}
private:
  A(const A &);
};

struct B {
  A a;
  B() : a(A()) {} // OK in C++17, error in C++14
};

Summary

I hope these notes will be useful for developers migrating to GCC 11. As with every major release, we've added new warnings that might turn up when switching compilers. Should you find a bug in these new warnings, please don't hesitate to open a new problem report as outlined in the GCC bugs page.

Comments