GrammaTalk

New Features of C++17

Posted on

by

Since C++11, WG21 (the ISO designation for the C++ Standards Committee) has been focusing on shipping a new standard every three years. The standard is comprised of two primary parts: the core language, and the Standard Template Library. In addition to shipping the International Standard, the committee produces Technical Specifications for core language or library features that are still experimental so that the community can gain implementation experience and user feedback before rolling the features into the International Standard. In keeping with the three-year schedule, the newest C++ standard, C++17, is anticipated to be published by ISO later this year. This is a brief tour of several of the new features of the core language.

c17_grammatech-1.jpg

Exception specifications

Up until now, C++ has had two different ways to specify whether or not an exception will escape a function: dynamic exception specifications and noexcept specifiers.

  void dynamic_exception_specifier_throwing() throw(std::exception);
void dynamic_exception_specifier_nonthrowing() throw()
void noexcept_specifier_throwing() noexcept(false);
void noexcept_specifier_nonthrowing() noexcept; // or, noexcept(true)

Use of dynamic exception specifications was deprecated in C++11 when noexcept specifiers were introduced, and dynamic exception specifications are now removed in C++17. However, the dynamic exception specification throw() is still permitted, despite being deprecated, as an alias for noexcept(true).

An interesting change to exception specifications is that they are now part of the function type itself, meaning that you can overload functions or produce template specializations based on the exception specification. For instance:

  void nonthrowing_func() noexcept {}
  void throwing_func() noexcept(false) {}

  void call_func_ptr(void (*fp)() noexcept) noexcept {
    fp();
  }

  template <typename Ty>
  void call_func_ptr2(Ty) {}

  template <>
  void call_func_ptr2(void (*fp)() noexcept) {}

  void f() {
    call_func_ptr(nonthrowing_func); // OK
    call_func_ptr(throwing_func); // Error

    call_func_ptr2(nonthrowing_func); // Calls template specialization
    call_func_ptr2(throwing_func); // Calls primary template
  }

Fold expressions

Generic code will sometimes make use of template parameter packs as a type-safe replacement for variadic functions. Fold expressions allow you to apply the same unary or binary operator to all elements of a parameter pack without going to the trouble of manually expanding the pack. For instance, if you were writing a generic function to sum all of the parameters (with the requirement that the types involved support the binary + operator and the sum function requires one or more arguments), you would manually write that code in C++14 as:

  template <typename Ty>
  Ty sum(Ty one) { return one; }

  template <typename Ty, typename… Args>
  auto sum(Ty one, Args… args) {
    return one + sum(args…);
  }

However, with fold expressions in C++17, that code can be reduced to:

  template <typename… Ty>
  auto sum(Ty… args) {
    return (args + …);
  }

Fold expressions come in two flavors: unary folds (as shown above) and binary folds, with the distinction being the number of arguments to the fold expression. Binary folds can be useful in circumstances where you want control over the initial element in the fold, such as in this example where we want to stream all of the arguments to the standard output stream:

  #include <iostream>

  template <typename… Ty>
  void f(Ty… args) {
   (std::cout << … << args) << std::endl;
  }

Explicit deduction guides

Speaking of templates, we’re all hopefully familiar with the idea that a function can have its template arguments deduced from a function call without requiring the function call to explicitly specify the template arguments. For instance, this code should be unsurprising:

  template <typename Ty>
  void f(Ty);

  int main() {
    f(12); // No need to say f<int>(12);
  }

However, the same is not traditionally true of class templates because you cannot use the same type deduction trick with class constructors because there is no way to distinguish the types in a templated constructor from the types in the class. Consider this constructor accepting a pair of iterators, as might be seen in a container class:

  template <typename Ty>
  class C {
  public:
    template <typename Iter>
    C(Iter begin, Iter end) {}
  };

  int main() {
    int i[] = { 1, 2, 3, 4, 5 };
    C c(i, i + 5); // Error, cannot deduce Ty for class C
    C<int> c(i, i + 5); // OK, template argument explicitly supplied
  }

In C++17, you can now add explicit deduction guides to a class constructor to enable deduction of the class template types.

  #include <iterator>

  template <typename Ty>
  class C {
  public:

    // Typical templated constructor.
    template <typename Iter>
    C(Iter begin, Iter end) {}
  };

  // Declares the deduction guide.
  template <typename Iter>
  C(Iter begin, 

    Iter end) -> C<typenamestd::iterator_traits<Iter>::value_type>;

  int main() {
    int i[] = { 1, 2, 3, 4, 5 };
    C c(i, i + 5); // Ok
  }

The deduction guide has the same syntactic form as a trailing return type on the constructor, which specifies the concrete class type being deduced. The deduction guide cannot be written on a constructor definition nor can it be inlined within the class on the constructor declaration. The out-of-line deduction guide tells the compiler that a call to that constructor results in a type as specified by the trailing return type, which must be a specialization of the primary class template.

The Standard Template Library now includes class deduction guides for many common containers. For instance, you can now write std::pair p(42, ‘a’) to define a variable of type std::pair<int, char>.

Structured bindings

A common desire in code is to return multiple values from a function using an elegant syntax. Frequently this means using a std::tuple<> or structure to return the information from the function. However, the current way to decouple that returned data from its aggregate is not always elegant because it requires the types involved to support default construction. Further, there is no automatic way to get constituent parts out of a structure value. Consider:

  #include <tuple>

  struct S {
    explicit S(int) {}  
  };

  struct T {
    int i;
    S s;
    double d;
  };

  std::tuple<int, S, double> f() {
    return { 42, S(10), 1.2 };
  }

  T g() {
    return { 42, S(10), 1.2 };
  }

  int main() {
    int i1, i2;
    S s1, s2; // Error, no default constructor for S
    double d1, d2;
    std::tie(i1, s1, d1) = f();
    // Error, cannot initialize a tuple from an arbitrary struct

    std::tie(i2, s2, d2) = g(); 

    // Use i1, s1, et al.
  }

Structured bindings allow you to decompose aggregate data using a single declaration with automatic type deduction. The single declaration, previously referred to in the standard as the decomposition declaration, uses square brackets to surround the variables that bind to the aggregate members. You can use structured bindings to decouple structures, arrays, or tuple-like classes with a get<>() function.

  #include <tuple>

  struct S {
    explicit S(int) {}
  };

  struct T {
    int i;
    S s;
    double d;
  };

  std::tuple<int, S, double> f() {
    return { 42, S(10), 1.2 };
  }

  T g() {
    return { 42, S(10), 1.2 };
  }

  int main() {
    auto [i1, s1, d1] = f();
    auto [i2, s2, d2] = g();

    // Use i1, s1, et al.
  }

if/switch initializers

C++ has long supported the ability to declare a variable in the init-statement of an if statement, where the declared variable participates in the branch test and can be used by the body of the if statement. However, this pattern breaks down with more complex uses beyond implicit boolean conversions. This leads to introducing variables with a broader scope than required, or contortions to limit the scope of the variable:

  #include <mutex>
  #include <vector>

  void f(std::vector<int> &v, std::mutex &m) {
    { // Note the introduced scope for lock.
      std::lock_guard<std::mutex> lock(m);
      if (v.empty())
        v.push_back(42);
    }
    // … other code …
  }

C++17 introduces the ability for if and switch statements to declare and initialize a locally-scoped variable, in much the same way that for loops have always allowed.

  #include <mutex>
  #include <vector>

  void f(std::vector<int> &v, std::mutex &m) {
    if (std::lock_guard<std::mutex> lock(m); v.empty())
      v.push_back(42);

    // … other code …
  }

The variable declared in the selection statement initializer is scoped to the selection statement, which means this code snippet is functionally equivalent to the preceding one while being less code to write.

C++17 is a great evolutionary step for C++ and this is just a smattering of the new functionality present with the latest release of the C++ standard. In addition to the features discussed here, several Technical Specifications have been published, including: Concepts as a core language feature to enable programmers to constrain template parameters for clarified template metaprogramming, Coroutines as a core language feature for non-preemptive multithreading, Networking as a library feature to allow inter-application communications, and Ranges as a reimagining of the STL where iterators are paired together to form a range of values for algorithms and containers. As the committee gains experience with the adoption of these features, they are anticipated to be rolled into the working draft for the next release of C++, which is termed C++2a.


Like what you read? Download our white paper “Advanced Static Analysis for C++” to learn more.

{{cta(’42fd5967-604d-43ad-9272-63d7c9d3b48f’)}}

Related Posts

Check out all of GrammaTech’s resources and stay informed.

view all posts

Contact Us

Get a personally guided tour of our solution offerings. 

Contact US