What’s New In C++17 言語仕様

このノートでは C++17 で注目すべき言語仕様を学習する。すでに cpprefjp がそのへんをきれいに整理している。それを利用して、読みながら急所を記していくことにする。

タイピングの都合で訳語は cpprefjp のものと一部変更して記す。

変数・データ構造関係

16 進浮動小数点数リテラルのサポート

16 進浮動小数点数リテラルのサポート。これにより値を正確に表現できる。IEEE754 的なアレだ。

  • 書式は prefix 仮数部 指数部 suffix という構成だ。ここで

    • prefix は 0x または 0X のいずれか必須

    • 仮数部は整数部と小数部を . でつなげて書く。ただし文字は 16 進数文字が使える。

    • 指数部(掛ける 2 の何乗であるかを示す部位)は prefix 符号 整数という構成

      • prefix は p または P

      • 符号は + または -

      • 整数は十進数で記す

    • suffix は次のとおり

      • 指定がなければ double

      • f または F ならば float

      • l または L ならば long double

インライン変数

インライン関数の変数版が追加された。これにともない inline の意味も若干変化するようだ。さらに constexpr との絡みもある。

以下、すべての式はあるヘッダーファイルでなされるものとする。

  • inline type name = value; と宣言かつ定義することが許される。この場合、これを #include する翻訳単位すべてにおいて name で参照される実体は同一となる。

  • static inline type name = value; と宣言かつ定義することが許される。この場合には翻訳単位ごとに実体は別物になる。

  • 静的メンバー変数であって constexpr が付くものは暗黙的に inline となる。

  • 注意として constexpr が付く関数は暗黙的に inline 関数だが、 constexpr 変数に関してはそのようなことはない。

構造化束縛

Python における次のようなコードと同等の代入構文が追加。

values = (100, 200, 300,)
first, second, third = values

C++17 ではこのように書く:

auto [first, second, third] = value;
  • auto の部分には const/volatile, & を付けてもよい。

  • この記法を適用できるのは右辺が組み込み配列、構造体(の非静的メンバー変数)、 std::pair, std::tuple, etc.

  • 要素の順序はユーザー定義型ならばメンバー変数の宣言順序と、配列ならば位置の順序とそれぞれ等しい。

  • この機能の弱点

    • 未使用変数を指定する手段がない。その結果、不要なコピーが生じるかもしれない。

    • 入れ子の一括束縛はサポートされていない。

  • その他にも細かい規則がある。使うときに確認。

厳密な式の評価順

  • operator.(), operator->(), operator.*(), operator->*() の引数の評価順が引数、オブジェクトの順になる。

  • operator()(), 関数、コンストラクターの引数の評価順が引数リストの左から右に順番に評価される。

  • operator[](). operator>>(), operator<<() も左から右とする。

  • operator=() は右から左とする。

  • operator+=() などの代入を伴う二項演算子は右から左とする。

  • メンバー関数の形で提供される operator?() は、それが準じる組み込み演算子の評価順に合わせるものとする。

  • operator new() 系はメモリー確保を先に行い、それから初期化子の評価を行うものとする。

その他

  • braced-init-list による 直接 初期化における型推論。auto 変数の型を決定する規則が細かくなった。

    • braced-init-list が単一要素からなるときには、その要素の型を推論するものとする。

    • braced-init-list が同一の型を持たない要素複数からなるときは、コンパイルエラーとなる。例えば intdouble のような(昇格可能な)ケースでも不適合とする。

  • 属性 [[maybe_unused]], [[nodiscard]] が追加。後述。

  • 値のコピー省略を保証

    • 〈右辺値を変数の初期化のために使用する場合、コピーもムーブも省略する〉保証のことだが、C++ という言語の性質を考えると、生半可な理解でここに何かを書くことを避けたい。

    • 今まで別の新機能を試すのに書いたコードにおいて、この手のコンストラクターが呼び出されなかったことがあった。それがこの C++17 新機能によるものだとすれば、理解を誤っていることになる。これは怖い。

  • 参照メンバーや const メンバーをもつクラスに対する placement new の適用。

    • これについては背景がわからない。

  • enum class 変数の初期化のときに整数を用いることが許される。ただし次の条件をすべて満たすときに限る:

    • enum class に基底型が指定されている

    • 初期化リストが単一の要素からなる

    • 直接初期化である

    • 精度を失わない変換である

    cpprefjp のデモコードでは列挙子のない byte という scoped enum を定義している。このコードは上の条件をすべて説明してくれている。

  • operator new() のオーバーロード void* operator new(std::size_t, std::align_val_t) が追加。これにより alinas() を用いた自前の型を定義しなくても、記憶領域を動的に割り当てる際に直接 alignment を指定することができるかもしれない。

  • 集成体初期化の拡張において、その基底クラスに対しても初期化可能になる。

    • 基底クラスに対する braced-init-list を派生クラスに対するそれの中に入れ子にして書けばいいようだ。最初に現れる braced-init-list が(最初の)基底クラスに対する braced-init-list と解釈されるのだろう。

制御構文

if 文と switch 文の括弧の中で変数の初期化が許される

Python で言うところの := のような役割を果たすのだろうか?セミコロンを使うことになるので、書く手間は Python と同じ程度?

if(size_t n = v.size(); n < 10){
    // ...
}

// 初期化コードは optional
switch(; size_t n = v.size()){
case 0:
    // ...
}

属性 [[fallthrough]]

switch 文で case ラベルの処理が何かあり、その処理を break せずに次の case ラベルの処理を敢えてさせたいとする。このときまともなコンパイラーは警告を出す。それを抑止するために、コンパイラーが break を期待している行に [[fallthrough]] と書くことが許される。

これは使わないから覚えなくていい。

if constexpr

コンパイル時に if 文を評価させる構文だ。構文は if と括弧の間にキーワード constexpr を挟むだけの単純なものだ。使い方はふつうの constexpr と同程度に難しい。

if constexpr (condition){
    statement;
}

範囲 for ループにおける仕様変更

対象となる範囲の begin()end() の型が異なっていても OK となる。

//auto first = range.begin(), last = range.end();
auto first = range.begin();
auto last = range.end();
for(; first != last; ++first){
    statement;
}

ラムダ式

  • ラムダ式において、捕獲リストに *this を指定すると copy capture することになる。

    • オブジェクトをコピーした上で const になる。非 const メンバー関数を呼び出せない。それを避けるにはラムダ式を mutable にする。cpprefjp の例ではメンバー関数 void F::onFinish(int) が非 const であることに注意。

  • constexpr ラムダ式。もしラムダ式が定数式であるような場合にコンパイル時に評価させる。

    • ラムダ式関連の機能の学習は後回しにして一気にやる。

テンプレート

  • 畳み込みをサポート。〈可変引数テンプレートのパラメータパックに対して二項演算を累積的に行う〉機能。

    • cpprefjp のデモコードにおける sum(), sum0(), all(), print_all() のコードをよく見ておくこと。

    • 畳み込みを分類すると次のようになるようだ:

      • 単項演算子を用いるか二項演算子を用いるか

      • 左畳み込みか右畳み込みか(演算子のオペランドの評価順によって使い分けるのだろう)。

    • 畳み込み式は丸括弧で囲まれることで表すものとする。シンプルだ。

      template <typename... T>
      auto fold(T... args)
      {
          return (args op ...); // i.e. arg1 op ... op argN
      }
      
  • キーワード typename をテンプレートテンプレート引数に書くことが許される。今まで書けなかったのか。

  • クラステンプレートのテンプレート引数推論

    • オブジェクト生成時にコンストラクターへの実引数からクラステンプレートのテンプレート引数を推論する機能だ。例えば std::vector<int> v {1, 2, 3} のつもりで std::vector {1, 2, 3} と書ける。

    • この機能に伴い、推論補助という機能が追加。ここはあとでやる。

    • この機能に伴い、デフォルトテンプレート引数のみを持つクラステンプレートは、生成時に <...> 部分を省略できるようになった。

    • 標準ライブラリーでの適用例を習得しておくこと。

  • 非型テンプレート引数の型に auto が許される。実際に推論された型を欲しいときには decltype() を利用する。

  • 非型テンプレート引数リストの定数式評価が許される。

    • 具体的にはポインターが該当する。ポインター(や配列や関数)のうち、定数式評価が可能なものならば非型テンプレート引数として与えてよい。

    • これも消化し切れていない。のちほど。

  • using 宣言のパック展開。

    基底クラスのメンバーをまとめてパック展開する使い方が許される。cpprefjp より引用:

    #include <iostream>
    
    struct ForLong {
        void operator()(long v) {
            std::cout << "ForLong:" << v << std::endl;
        }
    };
    
    struct ForString {
        void operator()(const std::string& v) {
            std::cout << "ForString:" << v << std::endl;
        }
    };
    
    template <typename... T>
    struct ForAll : T... {
        using T::operator()...;
        void operator()(int v) {
            std::cout << "ForAll:" << v << std::endl;
        }
    };
    
  • 変数テンプレートの「デフォルトテンプレート」が許される。cpprefjp の例を一部改変:

    #include <cassert>
    
    template <typename T=int>
    T x;
    
    int main()
    {
        auto y = x<>;
        assert(y == 0);
        return 0;
    }
    

定数式

  • static_assert() の第二引数の省略が許される。元ネタの assert() に引数が一つしかないのだから考えられる。

  • 先述したように constexpr ラムダ式が使えるようになる。

  • 先述したように if constexpr 文が使えるようになる。

名前空間

  • ダブルコロンを連結した名前を書くことでスコープを入れ子にすることなく部分名前空間を定義することが許される。

    namespace aaa::bbb::cc
    {
        // ...
    }
    
  • 名前空間に対して属性を与えることが許される。

    namespace [[deprecated]] aaa
    {
        // ...
    }
    
  • using ディレクティブでパック展開が許される。

    次のように using 宣言の行にカンマ区切りで識別子を並べることができる。

    using std::cout, std::endl;
    

例外

  • 関数の型に例外仕様が含まれるようになった。

    • ここで言う例外仕様とは noexcept() によるものしか差さない。旧式の throw() はもう忘れろ。

    • noexcept(false) な関数ポインターを noexcept(true) な関数ポインターにキャストすることは許されない。端的に言うと、noexcept の違いしかない関数を多重定義することは許されない。

    • ラムダ式の型においてもこの仕様が適用される。

    • この仕様変更により、C++14 まで適法だったコードが違法になることもある。

  • 旧式の例外仕様削除。つまり throw(xxx) と書けなくなる。代わりに noexcept(bool) を利用することができる。例外を送出するか否かが本質的なのだ。

属性

  • 属性 [[fallthrough]] が追加。先述のとおり。

  • 属性 [[maybe_unused]] が追加。コンパイラーの未使用変数の警告を抑止する。

    [[maybe_unused]] int x = 0;
    [[maybe_unused]] void f();
    
    template <class T>
    [[maybe_unused]] inline void g();
    
  • 属性 [[nodiscard]] が追加。関数の戻り値を呼び出し側が無視してはならないことを指示する。ユーザー定義型に与える方法と関数宣言に与える方法がある。

    struct [[nodiscard]] error_info {};
    
    error_info f() { return error_info{}; }
    
    [[nodiscard]] int g() { return 0; }
    
  • 名前空間に属性を与えることが許される。先述のとおり。

  • 列挙型の列挙子に属性を与えることが許される。その場合には列挙子とカンマの間に属性を記す。

  • 属性内の名前空間の指定をいちどにできる構文が追加。属性の先頭部分に using 名前空間 : の順に記述し、その後に続けて属性の名前を記述する。

    // [[CC::opt(1), CC::debug]] void f(){} と同じ
    [[using CC: opt(1), debug]] void f(){}
    
  • 標準が定義していない属性であり、コンパイラーにとっても不明な属性はコンパイラーは単に無視するものとする。

プリプロセッサ

__has_include という関数型マクロが追加される。これはインクルードするファイルが存在するかを確認するのに用いられる。従来は、欲しいヘッダーファイルに定義されている定数が定義されているか、のようなテストでその存在を確認していた。今回追加のこのマクロにより、コンパイラー(プリプロセッサー)がヘッダーファイル自体の存在をテストすることができるようになる。

使わなそうだから習得しなくていいだろう。

削除

  • トライグラフ削除。これは使わなかったはずなので気にしなくていい。

  • キーワード register の削除。これが修飾する変数は文字通りレジスターに格納されるという振る舞いだったはずだが、マニアックなライブラリー実装者くらいしか使うことはなかったのでは?

  • 演算子 bool::operator++ 前置後置どちらも削除。こんなオーバーロードがあったとは知らなかった。

    • cpprefjp に書いてあるテキストが面白い。

  • 先述したように旧式の例外仕様は廃止。

小さな変更

  • 定義済みマクロ

    • マクロ __cplusplus の値が 201703L に更新。

    • マクロ `__STDCPP_DEFAULT_NEW_ALIGNMENT__ が追加。使わないので忘れていい。

  • 機能テストマクロ。C++17 の機能がサポートされているかを判定するのに用いる。量が多いので割愛。

  • 次の条件を満たす例外仕様のあるラムダ式から関数ポインターに変換する際に、変換後のものに同等の例外仕様を与えるものとする。

    • ラムダ式はキャプチャーを持たない。

    • ラムダ式は汎用ラムダ式ではない。

  • UTF-8 文字リテラル(文字列ではなく文字)が許される。

    • u8'A' のような書き方をすればいい。

    • ただしコードポイントの範囲に制限がある。char の表現できるサイズに収まらなければならない。