はじめに

本書は2017年に規格制定されたプログラミング言語C++の国際規格、ISO/IEC 14882:2017の新機能をほぼすべて解説している。

新しいC++17は不具合を修正し、プログラマーの日々のコーディングを楽にする新機能がいくつも追加された。その結果、C++の特徴であるパフォーマンスや静的型付けは損なうことなく、近年の動的な型の弱い言語に匹敵するほどの柔軟な記述を可能にしている。

人によっては、新機能を学ぶのは労多くして益少なしと考えるかもしれぬが、C++の新機能は現実の問題を解決するための便利な道具として追加されるもので、仮に機能を使わないとしても問題はなくならないため、便利な道具なく問題に対処しなければならぬ。また、C++の機能は一般的なプログラマーにとって自然だと感じるように設計されているため、利用は難しくない。もしC++が難しいと感じるのであれば、それはC++が解決すべき現実の問題が難しいのだ。なんとなれば、我々は理想とは程遠い歪なアーキテクチャのコンピューターを扱う時代に生きている。CPUの性能上昇は停滞し、メモリはCPUに比べて遥かに遅く、しかもそのアクセスは定数時間ではない。キャッシュに収まる局所性を持つデータへの操作は無料同然で、キャッシュサイズの単位はすでにMBで数えられている。手のひらに乗る超低電力CPUでさえマルチコアが一般的になり、並列処理、非同期処理は全プログラマーが考慮せねばならぬ問題になった。

そのような時代にあたっては、かつては最良であった手法はその価値を失い、あるいは逆に悪い手法と成り下がる。同時に昔は現実的ではなかった手法が今ではかえってまともな方法になることさえある。このため、現在活発に使われている生きている言語は、常に時代に合わない機能を廃止し、必要な機能を追加する必要がある。C++の発展はここで留まることなく、今後もC++が使われ続ける限り、修正と機能追加が行われていくだろう。

本書の執筆はGithub上で公開して行われた。

https://github.com/EzoeRyou/cpp17book

本書のライセンスはGPLv3だ。

本書の執筆では株式会社ドワンゴとGitHub上でPull Requestを送ってくれた多くの貢献者の協力によって、誤りを正し、より良い記述を実現できた。この場を借りて謝意を表したい。

本書に誤りを見つけたならば、Pull Requestを送る先はhttps://github.com/EzoeRyou/cpp17bookだ。

江添亮

C++の規格

プログラミング言語C++はISOの傘下で国際規格ISO/IEC 14882として制定されている。この規格は数年おきに改定されている。一般にC++の規格を参照するときは、規格が制定した西暦の下二桁を取って、C++98(1998年発行)とかC++11(2011年発行)と呼ばれている。現在発行されているC++の規格は以下のとおり。

C++98

C++98は1998年に制定された最初のC++の規格である。本来ならば1994年か1995年には制定させる予定が大幅にずれて、1998年となった。

C++03

C++03はC++98の文面の曖昧な点を修正したマイナーアップデートとして2003年に制定された。新機能の追加はほとんどない。

C++11

C++11は制定途中のドラフト段階では元C++0xと呼ばれていた。これは、200x年までに規格が制定される予定だったからだ。予定は大幅に遅れ、ようやく規格が制定されたときにはすでに2011年の年末になっていた。C++11ではとても多くの新機能が追加された。

C++14

C++14は2014年に制定された。C++11の文面の誤りを修正した他、少し新機能が追加された。本書で解説する。

C++17

C++17は2017年に制定されることが予定されている最新のC++規格で、本書で解説する。

C++の将来の規格

C++20

C++20は2020年に制定されることが予定されている次のC++規格だ。この規格では、モジュール、コンセプト、レンジ、ネットワークに注力することが予定されている。

コア言語とライブラリ

C++の標準規格は、大きく分けて、Cプリプロセッサーとコア言語とライブラリからなる。

Cプリプロセッサーとは、C++がC言語から受け継いだ機能だ。ソースファイルをトークン列単位で分割して、トークン列の置換ができる。

コア言語とは、ソースファイルに書かれたトークン列の文法とその意味のことだ。

ライブラリとは、コア言語機能を使って実装されたもので、標準に提供されているものだ。標準ライブラリには、純粋にコア言語の機能のみで実装できるものと、それ以外の実装依存の方法やコンパイラーマジックが必要なものとがある。

SD-6 C++のための機能テスト推奨

C++17には機能テストのためのCプリプロセッサー機能が追加された。

機能テストマクロ

機能テストというのは、C++の実装(C++コンパイラー)が特定の機能をサポートしているかどうかをコンパイル時に判断できる機能だ。本来、C++17の規格に準拠したC++実装は、C++17の機能をすべてサポートしているべきだ。しかし、残念ながら現実のC++コンパイラーの開発はそのようには行われていない。C++17に対応途中のC++コンパイラーは将来的にはすべての機能を実装することを目標としつつも、現時点では一部の機能しか実装していないという状態になる。

たとえば、C++11で追加されたrvalueリファレンスという機能に現実のC++コンパイラーが対応しているかどうかをコンパイル時に判定するコードは以下のようになる。

#ifndef __USE_RVALUE_REFERENCES
  #if (__GNUC__ > 4 || __GNUC__ == 4 && __GNUC_MINOR__ >= 3) || \
      _MSC_VER >= 1600
    #if __EDG_VERSION__ > 0
      #define __USE_RVALUE_REFERENCES (__EDG_VERSION__ >= 410)
    #else
      #define __USE_RVALUE_REFERENCES 1
    #endif
  #elif __clang__
    #define __USE_RVALUE_REFERENCES __has_feature(cxx_rvalue_references)
  #else
    #define __USE_RVALUE_REFERENCES 0
  #endif
#endif

このそびえ立つクソのようなコードは現実に書かれている。このコードはGCCとMSVCとEDGとClangという現実に使われている主要な4つのC++コンパイラーに対応したrvalueリファレンスが実装されているかどうかを判定する機能テストコードだ。

この複雑なプリプロセッサーを解釈した結果、__USE_RVALUE_REFERENCESというプリプロセッサーマクロの値が、もしC++コンパイラーがrvalueリファレンスをサポートしているならば1、そうでなければ0となる。後は、このプリプロセッサーマクロで#ifガードしたコードを書く。

// 文字列を処理する関数
void process_string( std::string const & str ) ;

#if __USE_RVALUE_REFERENCES == 1
// 文字列をムーブして処理してよい実装の関数
// C++コンパイラーがrvalueリファレンスを実装していない場合はコンパイルされない
void process_string( std::string && str ) ;
#endif

C++17では、上のようなそびえ立つクソのようなコードを書かなくてもすむように、標準の機能テストマクロが用意された。C++実装が特定の機能をサポートしている場合、対応する機能テストマクロが定義される。機能テストマクロの値は、その機能がC++標準に採択された年と月を合わせた6桁の整数で表現される。

たとえばrvalueリファレンスの場合、機能テストマクロの名前は__cpp_rvalue_referencesとなっている。rvalueリファレンスは2006年10月に採択されたので、機能テストマクロの値は200610という値になっている。将来rvalueリファレンスの機能が変更されたときは機能テストマクロの値も変更される。この値を調べることによって使っているC++コンパイラーはいつの時代のC++標準の機能をサポートしているか調べることもできる。

この機能テストマクロを使うと、上のコードの判定は以下のように書ける。

// 文字列を処理する関数
void process_string( std::string const & str ) ;

#ifdef __cpp_rvalue_references
// 文字列をムーブして処理してよい実装の関数
// C++コンパイラーがrvalueリファレンスを実装していない場合はコンパイルされない
void process_string( std::string && str ) ;
#endif

機能テストマクロの値は通常は気にする必要がない。機能テストマクロが存在するかどうかで機能の有無を確認できるので、通常は#ifdefを使えばよい。

__has_include式 : ヘッダーファイルの存在を判定する

__has_include式は、ヘッダーファイルが存在するかどうかを調べるための機能だ。

__has_include( ヘッダー名 )

__has_include式はヘッダー名が存在する場合1に、存在しない場合0に置換される。

たとえば、C++17の標準ライブラリにはファイルシステムが入る。そのヘッダー名は<filesystem>だ。C++コンパイラーがファイルシステムライブラリをサポートしているかどうかを調べるには、以下のように書く。

#if __has_include(<filesystem>) 
// ファイルシステムをサポートしている
#include <filesystem>
namespace fs = std::filesystem ;
#else
// 実験的な実装を使う
#include <experimental/filesystem>
namespace fs = std::experimental::filesystem ;
#endif

C++実装が__has_includeをサポートしているかどうかは、__has_includeの存在をプリプロセッサーマクロのように#ifdefで調べることによって判定できる。

#ifdef __has_include
// __has_includeをサポートしている
#else
// __has_includeをサポートしていない
#endif

__has_include式は#if#elifの中でしか使えない。

int main()
{
    // エラー
    if ( __has_include(<vector>) )
    { }
}

__has_cpp_attribute式

C++実装が特定の属性トークンをサポートしているかどうかを調べるには、__has_cpp_attribute式が使える。

__has_cpp_attribute( 属性トークン )

__has_cpp_attribute式は、属性トークンが存在する場合は属性トークンが標準規格に採択された年と月を表す数値に、存在しない場合は0に置換される。

// [[nodiscard]]がサポートされている場合は使う
#if __has_cpp_attribute(nodiscard)
[[nodiscard]]
#endif
void * allocate_memory( std::size_t size ) ;

__has_include式と同じく、__has_cpp_attribute式も#if#elifの中でしか使えない。#ifdef__has_cpp_attribute式の存在の有無を判定できる。

C++14のコア言語の新機能

C++14で追加された新機能は少ない。C++14はC++03と同じくマイナーアップデートという位置付けで積極的な新機能の追加は見送られたからだ。

二進数リテラル

二進数リテラルは整数リテラルを二進数で記述する機能だ。整数リテラルのプレフィクスに0Bもしくは0bを書くと、二進数リテラルになる。整数を表現する文字は0と1しか使えない。

int main()
{
    int x1 = 0b0 ; // 0
    int x2 = 0b1 ; // 1
    int x3 = 0b10 ; // 2
    int x4 = 0b11001100 ; // 204
}

二進数リテラルは浮動小数点数リテラルには使えない。

機能テストマクロは__cpp_binary_literals, 値は201304。

数値区切り文字

数値区切り文字は、整数リテラルと浮動小数点数リテラルの数値をシングルクオート文字で区切ることができる機能だ。区切り桁は何桁でもよい。

int main()
{
    int x1 = 123'456'789 ;
    int x2 = 1'2'3'4'5'6'7'8'9 ; 
    int x3 = 1'2345'6789 ;
    int x4 = 1'23'456'789 ;

    double x5 = 3.14159'26535'89793 ;
}

大きな数値を扱うとき、ソースファイルに1000000001000000000と書かれていた場合、どちらが大きいのか人間の目にはわかりにくい。人間が読んでわかりにくいコードは間違いの元だ。数値区切りを使うと、100'000'0001'000'000'000のように書くことができる。これはわかりやすい。

他には、1バイト単位で見やすいように区切ることもできる。

int main()
{
    unsigned int x1 = 0xde'ad'be'ef ;
    unsigned int x2 = 0b11011110'10101101'10111110'11101111 ;
}

数値区切りはソースファイルを人間が読みやすくするための機能で、数値に影響を与えない。

[[deprecated]]属性

[[deprecated]]属性は名前とエンティティが、まだ使えるものの利用は推奨されない状態であることを示すのに使える。[[deprecated]]属性が指定できる名前とエンティティは、クラス、typedef名、変数、非staticデータメンバー、関数、名前空間、enum, enumerator, テンプレートの特殊化だ。

それぞれ以下のように指定できる。

// 変数
// どちらでもよい
[[deprecated]] int variable_name1 { } ;
int variable_name2 [[deprecated]] { } ;

// typedef名
[[deprecated]] typedef int typedef_name1 ;
typedef int typedef_name2 [[deprecated]] ;
using typedef_name3 [[deprecated]] = int ;

// 関数
// メンバー関数も同じ文法
// どちらでもよい
[[deprecated]] void function_name1() { }
void function_name2 [[deprecated]] () { }


// クラス
// unionも同じ
class [[deprecated]] class_name
{
// 非staticデータメンバー
[[deprecated]] int non_static_data_member_name ;
} ;

// enum
enum class [[deprecated]] enum_name
{
// enumerator
enumerator_name [[deprecated]] = 42
} ;


// 名前空間
namespace [[deprecated]] namespace_name { int x ; }

// テンプレートの特殊化

template < typename T >
class template_name { } ;

template < >
class [[deprecated]] template_name<void> { } ;

[[deprecated]]属性が指定された名前やエンティティを使うと、C++コンパイラーは警告メッセージを出す。

[[deprecated]]属性には、文字列を付け加えることができる。これはC++実装によっては警告メッセージに含まれるかもしれない。

[[deprecated("Use of f() is deprecated. Use f(int option) instead.")]]
void f() ;

void f( int option ) ;

機能テストマクロは__has_cpp_attribute(deprecated), 値は201309。

通常の関数の戻り値の型推定

関数の戻り値の型としてautoを指定すると、戻り値の型をreturn文から推定してくれる。

// int ()
auto a(){ return 0 ; }
// double ()
auto b(){ return 0.0 ; }

// T(T)
template < typename T >
auto c(T t){ return t ; }

return文の型が一致していないとエラーとなる。

auto f()
{
    return 0 ; // エラー、一致していない
    return 0.0 ; // エラー、一致していない
}

すでに型が決定できるreturn文が存在する場合、関数の戻り値の型を参照するコードも書ける。

auto a()
{
    &a ; // エラー、aの戻り値の型が決定していない
    return 0 ;
}

auto b()
{
    return 0 ;
    &b ; // OK、戻り値の型はint
}

関数aへのポインターを使うには関数aの型が決定していなければならないが、return文の前に型は決定できないので関数aはエラーになる。関数breturn文が現れた後なので戻り値の型が決定できる。

再帰関数も書ける。

auto sum( unsigned int i )
{
    if ( i == 0 )
        return i ; // 戻り値の型はunsigned int
    else
        return sum(i-1)+i ; // OK
}

このコードも、return文の順番を逆にすると戻り値の型が決定できずエラーとなるので注意。

auto sum( unsigned int i )
{
    if ( i != 0 )
        return sum(i-1)+i ; // エラー
    else
        return i ;
}

機能テストマクロは__cpp_return_type_deduction, 値は201304。

decltype(auto) : 厳格なauto

警告:この項目はC++規格の詳細な知識を解説しているため極めて難解になっている。平均的なC++プログラマーはこの知識を得てもよりよいコードが書けるようにはならない。この項目は読み飛ばすべきである。

decltype(auto)auto指定子の代わりに使える厳格なautoだ。利用にはC++の規格の厳格な理解が求められる。

autodecltype(auto)は型指定子と呼ばれる文法の一種で、プレイスホルダー型として使う。

わかりやすく言うと、具体的な型を式から決定する機能だ。

// aはint
auto a = 0 ;
// bはint 
auto b() { return 0 ; } 

変数宣言にプレイスホルダー型を使う場合、型を決定するための式は初期化子と呼ばれる部分に書かれる式を使う。関数の戻り値の型推定にプレイスホルダー型を使う場合、return文の式を使う。

decltype(auto)autoの代わりに使うことができる。decltype(auto)も型を式から決定する。

// aはint
decltype(auto) a = 0 ;
// bはint
decltype(auto) b() { return 0 ; }

一見するとautodecltype(auto)は同じようだ。しかし、この2つは式から型を決定する方法が違う。どちらもC++の規格の極めて難しい規則に基づいて決定される。習得には熟練の魔法使いであることが要求される。

autoが式から型を決定するには、autoキーワードをテンプレートパラメーター名で置き換えた関数テンプレートの仮引数に、式を実引数として渡してテンプレート実引数推定を行わせた場合に推定される型が使われる。

たとえば

auto x = 0 ;

の場合は、

template < typename T >
void f( T u ) ;

のような関数テンプレートに対して、

f(0) ;

と実引数を渡したときにuの型として推定される型と同じ型になる。

int i ;
auto const * x = &i ;

の場合には、

template < typename T >
void f( T const * u ) ;

のような関数テンプレートに

f(&i) ;

と実引数を渡したときにuの型として推定される型と同じ型になる。この場合はint const *になる。

ここまでがautoの説明だ。decltype(auto)の説明は簡単だ。

decltype(auto)の型は、autoを式で置き換えたdecltypeの型になる。

// int
decltype(auto) a = 0 ;

// int
decltype(auto) f() { return 0 ; }

上のコードは、下のコードと同じ意味だ。

decltype(0) a = 0 ;
decltype(0) f() { return 0 ; }

ここまでは簡単だ。そして、これ以降は黒魔術のようなC++の規格の知識が必要になってくる。

autodecltype(auto)は一見すると同じように見える。型を決定する方法として、autoは関数テンプレートの実引数推定を使い、decltype(auto)decltypeを使う。どちらも式を評価した結果の型になる。いったい何が違うというのか。

主な違いは、autoは関数呼び出しを使うということだ。関数呼び出しの際にはさまざまな暗黙の型変換が行われる。

たとえば、配列を関数に渡すと、暗黙の型変換の結果、配列の先頭要素へのポインターになる。

template < typename T >
void f( T u ) {}

int main()
{
    int array[5] ;
    // Tはint *
    f( array ) ;
}

ではautodecltype(auto)を使うとどうなるのか。

int array[5] ;
// int *
auto x1 = array ;
// エラー、配列は配列で初期化できない
decltype(auto) x2 = array ;

このコードは、以下と同じ意味になる。

int array[5] ;
// int *
int * x1 = array ;
// エラー、配列は配列で初期化できない
int x2[5] = array ;

autoの場合、型はint *となる。配列は配列の先頭要素へのポインターへと暗黙に変換できるので、結果のコードは正しい。

decltype(auto)の場合、型はint [5]となる。配列は配列で初期化、代入ができないので、このコードはエラーになる。

関数型も暗黙の型変換により関数へのポインター型になる。

void f() ;

// 型はvoid(*)()
auto x1 = f ;
// エラー、関数型は変数にできない
decltype(auto) x2 = f ;

autoはトップレベルのリファレンス修飾子を消すが、decltype(auto)は保持する。

int & f()
{
    static int x ;
    return x ;
}

int main()
{
    // int
    auto x1 = f() ;
    // int &
    decltype(auto) x2 = f() ;
}

リスト初期化はautoではstd::initializer_listだが、decltype(auto)では式ではないためエラー。

int main()
{
    // std::initializer_list<int>
    auto x1 = { 1,2,3 } ;
    // エラー、decltype({1,2,3})はできない
    decltype(auto) x2 = { 1,2,3 } ;
}

decltype(auto)は単体で使わなければならない。

// OK
auto const x1 = 0 ; 
// エラー
decltype(auto) const x2 = 0 ;

この他にもautodecltype(auto)にはさまざまな違いがある。すべての違いを列挙するのは煩雑なので省略するが、decltype(auto)は式の型を直接使う。autoはたいていの場合は便利な型の変換が入る。

autoは便利でたいていの場合はうまくいくが暗黙の型の変換が入るため、意図どおりの推定をしてくれないことがある。

たとえば、引数でリファレンスを受け取り、戻り値でそのリファレンスを返す関数を書くとする。以下のように書くのは間違いだ。

// int ( int & )
auto f( int & ref )
{ return ref ; }

なぜならば、戻り値の型は式の型から変化してintになってしまうからだ。ここでdecltype(auto)を使うと、

// int & ( int & )
decltype(auto) f( int & ref )
{ return ref ; }

式の型をそのまま使ってくれる。

ラムダ式にdecltype(auto)を使う場合は以下のように書く。

[]() -> decltype(auto) { return 0 ; } ;

decltype(auto)は主に関数の戻り値の型推定で式の型をそのまま推定してくれるようにするために追加された機能だ。その利用にはC++の型システムの深い理解が必要になる。

機能テストマクロは__cpp_decltype_auto, 値は201304。

ジェネリックラムダ

ジェネリックラムダはラムダ式の引数の型を書かなくてもすむようにする機能だ。

通常のラムダ式は以下のように書く。

int main()
{
    []( int i, double d, std::string s ) { } ;
}

ラムダ式の引数には型が必要だ。しかし、クロージャーオブジェクトのoperator ()に渡す型はコンパイル時にわかる。コンパイル時にわかるということはわざわざ人間が指定する必要はない。ジェネリックラムダを使えば、引数の型を書くべき場所にautoキーワードを書くだけで型を推定してくれる。

int main()
{
    []( auto i, auto d, auto s ) { } ;
}

ジェネリックラムダ式の結果のクロージャー型には呼び出しごとに違う型を渡すことができる。

int main()
{
    auto f = []( auto x ) { std::cout << x << '\n' ; } ;

    f( 123 ) ; // int
    f( 12.3 ) ; // double
    f( "hello" ) ; // char const *
}

仕組みは簡単で、以下のようなメンバーテンプレートのoperator ()を持ったクロージャーオブジェクトが生成されているだけだ。

struct closure_object
{
    template < typename T >
    auto operator () ( T x )
    {
        std::cout << x << '\n' ;
    }
} ;

機能テストマクロは__cpp_generic_lambdas, 値は201304。

初期化ラムダキャプチャー

初期化ラムダキャプチャーはラムダキャプチャーする変数の名前と式を書くことができる機能だ。

ラムダ式は書かれた場所から見えるスコープの変数をキャプチャーする。

int main()
{
    int x = 0 ;
    auto f = [=]{ return x ; } ;
    f() ;
}

初期化ラムダキャプチャーはラムダキャプチャーに初期化子を書くことができる機能だ。

int main()
{
    int x = 0 ;
    [ x = x, y = x, &ref = x, x2 = x * 2 ]
    {// キャプチャーされた変数を使う
        x ;
        y ;
        ref ;
        x2 ;
    } ;
}

初期化ラムダキャプチャーは、"識別子 = expr" という文法でラムダ導入子[]の中に書く。するとあたかも"auto 識別子 = expr ;"と書いたかのように変数が作られる。これによりキャプチャーする変数の名前を変えたり、まったく新しい変数を宣言することができる。

初期化ラムダキャプチャーの識別子の前に&を付けると、リファレンスキャプチャー扱いになる。

int main()
{
    int x = 0 ;
    [ &ref = x ]()
    {
        ref = 1 ;
    }() ;

    // xは1
}

初期化ラムダキャプチャーが追加された理由には変数の名前を変えたりまったく新しい変数を導入したいという目的の他に、非staticデータメンバーをコピーキャプチャーするという目的がある。

以下のコードには問題があるが、わかるだろうか。

struct X
{
    int data = 42 ;

    auto get_closure_object()
    {
        return [=]{ return data ; } ;
    }
} ;


int main()
{
    std::function< int() > f ;

    {
        X x ;
        f = x.get_closure_object() ;
    }

    std::cout << f() << std::endl ;
}

X::get_closure_objectX::dataを返すクロージャーオブジェクトを返す。

auto get_closure_object()
{
    return [=]{ return data ; } ;
}

これを見ると、コピーキャプチャーである[=]を使っているので、dataはクロージャーオブジェクト内にコピーされているように思える。しかし、ラムダ式は非staticデータメンバーをキャプチャーしてはいない。ラムダ式がキャプチャーしているのはthisポインターだ。上のコードと下のコードは同じ意味になる。

auto get_closure_object()
{
    return [this]{ return this->data ; } ;
}

さて、main関数をもう一度見てみよう。

int main()
{
    // クロージャーオブジェクトを代入するための変数
    std::function< int() > f ;

    {
        X x ; // xが構築される
        f = x.get_closure_object() ;
        // xが破棄される
    }

    // すでにxは破棄された
    // return &x->dataで破棄されたxを参照する
    std::cout << f() << std::endl ;
}

なんと、すでに破棄されたオブジェクトへのリファレンスを参照してしまっている。これは未定義の動作だ。

初期化ラムダキャプチャーを使えば、非staticデータメンバーもコピーキャプチャーできる。

auto get_closure_object()
{
    return [data=data]{ return data ; } ;
}

なお、ムーブキャプチャーは存在しない。ムーブというのは特殊なコピーなので初期化ラムダキャプチャーがあれば実現できるからだ。

auto f()
{
    std::string str ;
    std::cin >> str ;
    // ムーブ
    return [str = std::move(str)]{ return str ; } ;
}

機能テストマクロは__cpp_init_captures, 値は201304。

変数テンプレート

変数テンプレートとは変数宣言をテンプレート宣言にできる機能だ。

template < typename T >
T variable { } ;

int main()
{
    variable<int> = 42 ;
    variable<double> = 1.0 ;
}

これだけではわからないだろうから、順を追って説明する。

C++ではクラスを宣言できる。

class X
{
    int member ;
} ;

C++ではクラスをテンプレート宣言できる。型テンプレートパラメーターは型として使える。

template < typename T >
class X
{
public :
    T member ;
} ;

int main()
{
    X<int> i ;
    i.member = 42 ; // int

    X<double> d ;
    d.member = 1.0 ; // double
}

C++では関数を宣言できる。

int f( int x )
{ return x ; }

C++では関数をテンプレート宣言できる。型テンプレートパラメーターは型として使える。

template < typename T >
T f( T x )
{ return x ; }

int main()
{
    auto i = f( 42 ) ; // int
    auto d = f( 1.0 ) ; // double
}

C++11ではtypedef名を宣言するためにエイリアス宣言ができる。

using type = int ;

C++11ではエイリアス宣言をテンプレート宣言できる。型テンプレートパラメーターは型として使える。

template < typename T >
using type = T ;

int main()
{
    type<int> i = 42 ; // int
    type<double> d = 1.0 ; // double
}

そろそろパターンが見えてきたのではないだろうか。C++では一部の宣言はテンプレート宣言できるということだ。このパターンを踏まえて以下を考えてみよう。

C++では変数を宣言できる。

int variable{} ;

C++14では変数宣言をテンプレート宣言できる。型テンプレートパラメーターは型として使える。

template < typename T >
T variable { } ;

int main()
{
    variable<int> = 42 ;
    variable<double> = 1.0 ;
}

変数テンプレートは名前どおり変数宣言をテンプレート宣言できる機能だ。変数テンプレートはテンプレート宣言なので、名前空間スコープとクラススコープの中にしか書くことができない。

// これはグローバル名前空間スコープという特別な名前空間スコープ

namespace ns {
// 名前空間スコープ
}

class
{
// クラススコープ
} ;

変数テンプレートの使い道は主に2つある。

意味は同じだが型が違う定数

プログラムでマジックナンバーを変数化しておくのは良い作法であるとされている。たとえば円周率を3.14...などと書くよりもpiという変数名で扱ったほうがわかりやすい。変数化すると、円周率の値が後で変わったときにプログラムを変更するのも楽になる。

constexpr double pi = 3.1415926535 ;

しかし、円周率を表現する型が複数ある場合どうすればいいのか。よくあるのは名前を分ける方法だ。

constexpr float pi_f = 3.1415 ;
constexpr double pi_d = 3.1415926535 ;
constexpr int pi_i = 3 ;
// 任意の精度の実数を表現できるクラスとする
const Real pi_r("3.141592653589793238462643383279") ;

しかしこれは、使う側で型によって名前を変えなければならない。

// 円の面積を計算する関数
template < typename T >
T calc_area( T r )
{
    // Tの型によって使うべき名前が変わる
    return r * r * ??? ;
}

関数テンプレートを使うという手がある。

template < typename T >
constexpr T pi()
{
    return static_cast<T>(3.1415926535) ;
}

template < >
Real pi()
{
    return Real("3.141592653589793238462643383279") ;
}


template < typename T >
T calc_area( T r )
{
    return r * r * pi<T>() ;
}

しかし、この場合引数は何もないのに関数呼び出しのための()が必要だ。

変数テンプレートを使うと以下のように書ける。

template < typename T >
constexpr T pi = static_cast<T>(3.1415926535) ;

template < >
Real pi<Real>("3.141592653589793238462643383279") ;

template < typename T >
T calc_area( T r )
{
    return r * r * pi<T> ;
}

traitsのラッパー

値を返すtraitsで値を得るには::valueと書かなければならない。

std::is_pointer<int>::value ;
std::is_same< int, int >::value ;

C++14ではstd::integral_constantconstexpr operator boolが追加されたので、以下のようにも書ける。

std::is_pointer<int>{} ;
std::is_same< int, int >{} ;

しかしまだ面倒だ。変数テンプレートを使うとtraitsの記述が楽になる。

template < typename T >
constexpr bool is_pointer_v = std::is_pointer<T>::value ;
template < typename T, typename U >
constexpr bool is_same_v = std::is_same<T, U>::value ;

is_pointer_v<int> ;
is_same_v< int, int > ;

C++の標準ライブラリでは従来のtraitsライブラリを変数テンプレートでラップした_v版を用意している。

機能テストマクロは__cpp_variable_templates, 値は201304。

constexpr関数の制限緩和

C++11で追加されたconstexpr関数はとても制限が強い。constexpr関数の本体には実質return文1つしか書けない。

C++14では、ほとんど何でも書けるようになった。

constexpr int f( int x )
{
    // 変数を宣言できる
    int sum = 0 ;

    // 繰り返し文を書ける
    for ( int i = 1 ; i < x ; ++i )
    {
        // 変数を変更できる
        sum += i ;
    }

    return sum ;
}

機能テストマクロは__cpp_constexpr, 値は201304。

C++11のconstexpr関数に対応しているがC++14のconstexpr関数に対応していないC++実装では、__cpp_constexprマクロの値は200704になる。

メンバー初期化子とアグリゲート初期化の組み合わせ

C++14ではメンバー初期化子とアグリゲート初期化が組み合わせられるようになった。

メンバー初期化子とはクラスの非staticデータメンバーを=で初期化できるC++11の機能だ。

struct S
{
    // メンバー初期化子
    int data = 123 ;
} ;

アグリゲート初期化とはアグリゲートの条件を満たす型をリスト初期化で初期化できるC++11の機能だ。

struct S
{
    int x, y, z ;
} ;

S s = { 1,2,3 } ;
// s.x == 1, s.y == 2, s.z == 3

C++11ではメンバー初期化子を持つクラスはアグリゲート型の条件を満たさないのでアグリゲート初期化ができない。

C++14では、この制限が緩和された。

struct S
{
    int x, y=1, z ;
} ;

S s1 = { 1 } ;
// s1.x == 1, s1.y == 1, s1.z == 0

S s2{ 1,2,3 } ;
// s2.x == 1, s2.y == 2, s2.z == 3

アグリゲート初期化で、メンバー初期化子を持つ非staticデータメンバーに対応する値がある場合はアグリゲート初期化が優先される。省略された場合はメンバー初期化子で初期化される。アグリゲート初期化でもメンバー初期化子でも明示的に初期化されていない非staticデータメンバーは空の初期化リストで初期化された場合と同じになる。

機能テストマクロは__cpp_aggregate_nsdmi, 値は201304。

サイズ付き解放関数

C++14ではoperator deleteのオーバーロードに、解放すべきストレージのサイズを取得できるオーバーロードが追加された。

void operator delete    ( void *, std::size_t ) noexcept ;
void operator delete[]  ( void *, std::size_t ) noexcept ;

第二引数はstd::size_t型で、第一引数で指定されたポインターが指す解放すべきストレージのサイズが与えられる。

たとえば以下のように使える。

void * operator new ( std::size_t size )
{
    void * ptr =  std::malloc( size ) ;

    if ( ptr == nullptr )
        throw std::bad_alloc() ;

    std::cout << "allocated storage of size: " << size << '\n' ;
    return ptr ;
}

void operator delete ( void * ptr, std::size_t size ) noexcept
{
    std::cout << "deallocated storage of size: " << size << '\n' ;
    std::free( ptr ) ;
}

int main()
{
    auto u1 = std::make_unique<int>(0) ;
    auto u2 = std::make_unique<double>(0.0) ;
}

機能テストマクロは__cpp_sized_deallocation, 値は201309。

C++17のコア言語の新機能

C++14の新機能のおさらいが終わったところで、いよいよC++17のコア言語の新機能を解説していく。

C++17のコア言語の新機能には、C++11ほどの大きなものはない。

トライグラフの廃止

C++17ではトライグラフが廃止された。

トライグラフを知らない読者はこの変更を気にする必要はない。トライグラフを知っている読者はなおさら気にする必要はない。

16進数浮動小数点数リテラル

C++17では浮動小数点数リテラルに16進数を使うことができるようになった。

16進数浮動小数点数リテラルは、プレフィクス0xに続いて仮数部を16進数(0123456789abcdefABCDEF)で書き、pもしくはPに続けて指数部を10進数で書く。

double d1 = 0x1p0 ; // 1
double d2 = 0x1.0p0 ; // 1
double d3 = 0x10p0 ; // 16
double d4 = 0xabcp0 ; // 2748

指数部はeではなくpPを使う。

double d1 = 0x1p0 ;
double d2 = 0x1P0 ;

16進数浮動小数点数リテラルでは、指数部を省略できない。

int a = 0x1 ; // 整数リテラル
0x1.0 ; // エラー、指数部がない

指数部は10進数で記述する。16進数浮動小数点数リテラルは仮数部に2の指数部乗を掛けた値になる。つまり、

0xNpM

という浮動小数点数リテラルの値は

\(N \times 2^M\)

となる。

0x1p0 ; // 1
0x1p1 ; // 2
0x1p2 ; // 4
0x10p0 ; // 16
0x10p1 ; // 32
0x1p-1 ; // 0.5
0x1p-2 ; // 0.25

16進数浮動小数点数リテラルには浮動小数点数サフィックスを記述できる。

auto a = 0x1p0f ; // float
auto b = 0x1p0l ; // long double

16進数浮動小数点数リテラルは、浮動小数点数が表現方法の詳細を知っている環境(たとえばIEEE--754)で、正確な浮動小数点数の表現が記述できるようになる。

機能テストマクロは__cpp_hex_float, 値は201603。

UTF-8文字リテラル

C++17ではUTF-8文字リテラルが追加された。

char c = u8'a' ;

UTF-8文字リテラルは文字リテラルにプレフィクスu8を付ける。UTF-8文字リテラルはUTF-8のコード単位1つで表現できる文字を扱うことができる。UCSの規格としては、C0制御文字と基本ラテン文字Unicodeブロックが該当する。UTF-8文字リテラルに書かれた文字が複数のUTF-8コード単位を必要とする場合はエラーとなる。

// エラー
// U+3042はUTF-8は0xE3, 0x81, 0x82という3つのコード単位で表現する必要が
// あるため
u8'あ' ;

機能テストマクロはない。

関数型としての例外指定

C++17では例外指定が関数型に組み込まれた。

例外指定とはnoexceptのことだ。noexceptnoexcept(true)が指定された関数は例外を外に投げない。

C++14ではこの例外指定は型システムに入っていなかった。そのため、無例外指定の付いた関数へのポインター型は型システムで無例外を保証することができなかった。

// C++14のコード
void f()
{
    throw 0 ; 
}

int main()
{
    // 無例外指定の付いたポインター
    void (*p)() noexcept = &f ;

    // 無例外指定があるにもかかわらず例外を投げる
    p() ;
}

C++17では例外指定が型システムに組み込まれた。例外指定のある関数型を例外指定のない関数へのポインター型に変換することはできる。逆はできない。

// 型はvoid()
void f() { }
// 型はvoid() noexcept
void g() noexcept { }

// OK
// p1, &fは例外指定のない関数へのポインター型
void (*p1)() = &f ;
// OK
// 例外指定のある関数へのポインター型&gを例外指定のない関数へのポインター型p2
// に変換できる
void (*p2)() = &g ; // OK

// エラー
// 例外指定のない関数へのポインター型&fは例外指定のある関数へのポインター型p3
// に変換できない
void (*p3)() noexcept = &f ;

// OK
// p4, &gは例外指定のある関数へのポインター型
void (*p4)() noexcept = &g ;

機能テストマクロは__cpp_noexcept_function_type, 値は201510。

fold式

C++17にはfold式が入った。foldは元は数学の概念で畳み込みとも呼ばれている。

C++におけるfold式とはパラメーターパックの中身に二項演算子を適用するための式だ。

今、可変長テンプレートを使って受け取った値をすべて加算した合計を返す関数sumを書きたいとする。

template < typename T, typename ... Types >
auto sum( T x, Types ... args ) ;

int main()
{
    int result = sum(1,2,3,4,5,6,7,8,9) ; // 45
}

このような関数テンプレートsumは以下のように実装することができる。

template < typename T >
auto sum( T x )
{
    return x ;
}

template < typename T, typename ... Types >
auto sum( T x, Types ... args )
{
    return x + sum( args... )  ;
}

sum(x, args)は1番目の引数をxで、残りをパラメーターパックargsで受け取る。そして、x + sum( args ... )を返す。すると、sum( args ... )はまたsum(x, args)に渡されて、1番目の引数、つまり最初から見て2番目の引数がxに入り、またsumが呼ばれる。このような再帰的な処理を繰り返していく。

そして、引数が1つだけになると、可変長テンプレートではないsumが呼ばれる。これは重要だ。なぜならば可変長テンプレートは0個の引数を取ることができるので、そのまま可変長テンプレート版のsumが呼ばれてしまうと、次のsumの呼び出しができずにエラーとなる。これを回避するために、また再帰の終了条件のために、引数が1つのsumのオーバーロード関数を書いておく。

可変長テンプレートでは任意個の引数に対応するために、このような再帰的なコードが必須になる。

しかし、ここで実現したいこととは\(N\)個あるパラメーターパックargsの中身に対して、仮に\(N\)番目をargs#Nとする表記を使うと、args#0 + args#1 + ... + args#N-1のような展開をしたいだけだ。C++17のfold式はパラメーターパックに対して二項演算子を適用する展開を行う機能だ。

fold式を使うとsumは以下のように書ける。

template < typename ... Types >
auto sum( Types ... args )
{
    return ( ... + args )  ;
}

( ... + args )は、args#0 + args#1 + ... + args#N-1のように展開される。

fold式には、単項fold式と二項fold式がある。そして、演算子の結合順序に合わせて左foldと右foldがある。

fold式は必ず括弧で囲まなければならない。

template < typename ... Types >
auto sum( Types ... args )
{
    // fold式
    ( ... + args )  ;
    // エラー、括弧がない
    ... + args ;
}

単項fold式の文法は以下のいずれかになる。

単項右fold
( cast-expression fold-operator ... )
単項左fold
( ... fold-operator cast-expression )

template < typename ... Types >
void f( Types ... args )
{
    // 単項左fold
    ( ... + args )  ;
    // 単項右fold
    ( args + ... ) ;
}

cast-expressionには未展開のパラメーターパックが入っていなければならない。

template < typename T >
T f( T x ) { return x ; }

template < typename ... Types >
auto g( Types ... args )
{
    // f(args#0) + f(args#1) + ... + f(args#N-1)
    return ( ... + f(args) )  ;
}

これはf(args)というパターンが展開される。

fold-operatorには以下のいずれかの二項演算子を使うことができる。

+   -   *   /   %   ^   &   |   <<  >>
+=  -=  *=  /=  %=  ^=  &=  |=  <<= >>=
==  !=  <   >   <=  >=  &&  ||  ,   .*  ->*

fold式には左foldと右foldがある。

fold式の( ... op pack )では、展開結果は((( pack#0 op pack#1 ) op pack#2 ) ... op pack#N-1 )となる。右fold式の( pack op ... )では、展開結果は( pack#0 op ( pack#1 op ( pack#2 op ( ... op pack#N-1 ))))となる。

template < typename ... Types >
void sum( Types ... args )
{
    // 左fold
    // ((((1+2)+3)+4)+5)
    auto left = ( ... + args ) ;
    // 右fold
    // (1+(2+(3+(4+5))))
    auto right = ( args + ... ) ;
}

int main()
{
    sum(1,2,3,4,5) ;
}

浮動小数点数のような交換法則を満たさない型にfold式を適用する際には注意が必要だ。

二項fold式の文法は以下のいずれかになる。

( cast-expression fold-operator ... fold-operator cast-expression )

左右のcast-expressionのどちらか片方だけに未展開のパラメーターパックが入っていなければならない。2つのfold-operatorは同じ演算子でなければならない。

( e1 op1 ... op2 e2 )という二項fold式があったとき、e1にパラメーターパックがある場合は二項右fold式、e2にパラメーターパックがある場合は二項左fold式になる。

template < typename ... Types >
void sum( Types ... args )
{
    // 左fold
    // (((((0+1)+2)+3)+4)+5)
    auto left = ( 0 + ... + args ) ;
    // 右fold
    // (1+(2+(3+(4+(5+0)))))
    auto right = ( args + ... + 0 ) ;
}

int main()
{
    sum(1,2,3,4,5) ;
}

fold式はパラメーターパックのそれぞれに二項演算子を適用したいときにわざわざ複雑な再帰的テンプレートを書かずにすむ方法を提供してくれる。

機能テストマクロは__cpp_fold_expressions, 値は201603。

ラムダ式で*thisのコピーキャプチャー

C++17ではラムダ式で*thisをコピーキャプチャーできるようになった。*thisをコピーキャプチャーするには、ラムダキャプチャーに*thisと書く。

struct X
{
   int data = 42 ;
   auto get()
   {
       return [*this]() { return this->data ; } ;
   }
} ;

int main()
{
    std::function < int () > f ;
    {
        X x ;
        f = x.get() ;
    }// xの寿命はここまで
    
    // コピーされているので問題ない
    int data = f() ;
}

コピーキャプチャーする*thisはラムダ式が書かれた場所の*thisだ。

また、以下のようなコードで挙動の違いを見るとわかりやすい。

struct X
{
   int data = 0 ;
   void f()
   {
        // thisはポインターのキャプチャー
        // dataはthisポインターをたどる
        [this]{ data = 1 ; }() ;

        // this->dataは1

        // エラー、*thisはコピーされている
        // クロージャーオブジェクトのコピーキャプチャーされた変数は
        // デフォルトで変更できない
        [*this]{ data = 2 ; } () ;

        // OK、mutableを使っている

        [*this]() mutable { data = 2 ; } () ;

        // this->dataは1
        // 変更されたのはコピーされたクロージャーオブジェクト内の*this        
   }
} ;

最初のラムダ式で生成されるクロージャーオブジェクトは以下のようなものだ。

class closure_object
{
    X * this_ptr ;

public :
    closure_object( X * this_ptr )
        : this_ptr(this_ptr) { }

    void operator () () const
    {
        this_ptr->data = 1 ;
    }
} ;

2番目のラムダ式では以下のようなクロージャーオブジェクトが生成される。

class closure_object
{
    X this_obj ;
    X const * this_ptr = &this_obj ;

public :
    closure_object( X const & this_obj )
        : this_obj(this_obj) { }

    void operator () () const
    {
        this_ptr->data = 2 ;
    }
} ;

これはC++の文法に従っていないのでやや苦しいコード例だが、コピーキャプチャーされた値を変更しようとしているためエラーとなる。

3番目のラムダ式では以下のようなクロージャーオブジェクトが生成される。

class closure_object
{
    X this_obj ;
    X * this_ptr = &this_obj ;

public :
    closure_object( X const & this_obj )
        : this_obj(this_obj) { }

    void operator () ()
    {
        this_ptr->data = 2 ;
    }
} ;

ラムダ式にmutableが付いているのでコピーキャプチャーされた値も変更できる。

*thisをコピーキャプチャーした場合、thisキーワードはコピーされたオブジェクトへのポインターになる。

struct X
{
   int data = 42 ;
   void f()
   {
        // thisはこのメンバー関数fを呼び出したオブジェクトへのアドレス
        std::printf("%p\n", this) ;

        // thisはコピーされた別のオブジェクトへのアドレス
        [*this](){  std::printf("%p\n", this) ;  }() ;
   }
} ;

int main()
{
    X x ;
    x.f() ;
}

この場合、出力される2つのポインターの値は異なる。

ラムダ式での*thisのコピーキャプチャーは名前どおり*thisのコピーキャプチャーを提供する提案だ。同等の機能は初期化キャプチャーでも可能だが、表記が冗長で間違いの元だ。

struct X
{
    int data ;

    auto f()
    {
        return [ tmp = *this ] { return tmp.data ; } ;
    }
} ;

機能テストマクロは__cpp_capture_star_this, 値は201603。

constexprラムダ式

C++17ではラムダ式がconstexprになった。より正確に説明すると、ラムダ式のクロージャーオブジェクトのoperator ()は条件を満たす場合constexprになる。

int main()
{
    auto f = []{ return 42 ; } ;

    constexpr int value = f() ; // OK
}

constexprの条件を満たすラムダ式はコンパイル時定数を必要とする場所で使うことができる。たとえばconstexpr変数や配列の添字やstatic_assertなどだ。

int main()
{
    auto f = []{ return 42 ; } ;

    int a[f()] ;
    static_assert( f() == 42 ) ;
    std::array<int, f()> b ;
}

constexprの条件を満たすのであれば、キャプチャーもできる。

int main()
{
    int a = 0 ; // 実行時の値
    constexpr int b = 0 ; // コンパイル時定数 

    auto f = [=]{ return a ; } ;
    auto g = [=]{ return b ; } ;

    // エラー、constexprの条件を満たさない
    constexpr int c = f() ;

    // OK、constexprの条件を満たす
    constexpr int d = g() ;
}

以下の内容は上級者向けの解説であり、通常の読者は理解する必要がない。

constexprラムダ式はSFINAEの文脈で使うことができない。

// エラー
template < typename T,
    bool b = []{
        T t ;
        t.func() ;
        return true ;
    }() ; >
void f()
{
    T t ;
    t.func() ;
}

なぜならば、これを許してしまうとテンプレート仮引数に対して任意の式や文がテンプレートのSubstitutionに失敗するかどうかをチェックできてしまうからだ。

上級者向けの解説終わり。

機能テストマクロは__cpp_constexpr, 値は201603。

__cpp_constexprマクロの値は、C++11の時点で200704、C++14の時点で201304だ。

文字列なしstatic_assert

C++17ではstatic_assertに文字列リテラルを取らないものが追加された。

static_assert( true ) ;

C++11で追加されたstatic_assertには、文字列リテラルが必須だった。

static_assert( true, "this shall not be asserted." ) ;

特に文字列を指定する必要がない場合もあるので、文字列リテラルを取らないstatic_assertが追加された。

機能テストマクロは__cpp_static_assert, 値は201411。

C++11の時点で__cpp_static_assertの値は200410。

ネストされた名前空間定義

C++17ではネストされた名前空間の定義を楽に書ける。

ネストされた名前空間とは、A::B::Cのように名前空間の中に名前空間が入っている名前空間のことだ。

namespace A {
    namespace B {
        namespace C {
        // ...
        }
    }
} 

C++17では、上記のコードと同じことを以下のように書ける。

namespace A::B::C {
// ...
}

機能テストマクロは__cpp_nested_namespace_definitions, 値は201411。

[[fallthrough]]属性

[[fallthrough]]属性はswitch文の中のcaseラベルを突き抜けるというヒントを出すのに使える。

switch文では対応するcaseラベルに処理が移る。通常、以下のように書く。

void f( int x )
{
    switch ( x )
    {
    case 0 :
        // 処理0
        break ;
    case 1 :
        // 処理1
        break ;
    case 2 :
        // 処理2
        break ;
    default :
        // xがいずれでもない場合の処理
        break ;
    }
}

この例を以下のように書くと

case 1 :
    // 処理1
case 2 :
    // 処理2
    break ;

xが1のときは処理1を実行した後に、処理2も実行される。switch文を書くときはこのような誤りを書いてしまうことがある。そのため、賢いC++コンパイラーはswitch文のcaseラベルでbreak文やreturn文などで処理が終わらず、次のcaseラベルやdefaultラベルに処理に突き抜けるコードを発見すると、警告メッセージを出す。

しかし、プログラマーの意図がまさに突き抜けて処理してほしい場合、警告メッセージは誤った警告となってしまう。そのような警告メッセージを抑制するため、またコード中に処理が突き抜けるという意図をわかりやすく記述するために、[[fallthrough]]属性が追加された。

case 1 :
    // 処理1
    [[fallthrough]]
case 2 :
    // 処理2
    break ;

[[fallthrough]]属性を書くと、C++コンパイラーは処理がその先に突き抜けることがわかるので、誤った警告メッセージを抑制できる。また、他人がコードを読むときに意図が明らかになる。

機能テストマクロは__has_cpp_attribute(fallthrough), 値は201603。

[[nodiscard]]属性

[[nodiscard]]属性は関数の戻り値が無視されてほしくないときに使うことができる。[[nodiscard]]属性が付与された関数の戻り値を無視すると警告メッセージが表示される。

[[nodiscard]] int f()
{
    return 0 ;
}

void g( int ) { }

int main()
{
    // エラー、戻り値が無視されている
    f() ;

    // OK、戻り値は無視されていない
    int result = f() ;
    g( f ) ;
    f() + 1 ;
    (void) f() ;
}

戻り値を無視する、というのは万能ではない。上の例でも、意味的には戻り値は無視されていると言えるが、コンパイラーはこの場合に戻り値が無視されているとは考えない。

[[nodiscard]]の目的は、戻り値を無視してほしくない関数をユーザーが利用したときの初歩的な間違いを防ぐためにある。void型にキャストするような意図的な戻り値の無視まで防ぐようには作られていない。

[[nodiscard]]属性を使うべき関数は、戻り値を無視してほしくない関数だ。どのような関数が戻り値を無視してほしくないかというと大きく2つある。

戻り値をエラーなどのユーザーが確認しなければならない情報の通知に使う関数。

enum struct error_code
{
    no_error, some_operations_failed,  serious_error
} ;

// 失敗するかもしれない処理
error_code do_something_that_may_fail()
{
    // 処理

    if ( is_error_condition() )
        return error_code::serious_error ;

    // 処理

    return error_code::no_error ;
}

// エラーがいっさい発生しなかったときの処理
int do_something_on_no_error() ;

int main()
{
    // エラーを確認していない
    do_something_that_may_fail() ;

    // エラーがない前提で次の処理をしようとする
    do_something_on_no_error() ;
}

関数に[[nodiscard]]属性を付与しておけば、このようなユーザー側の初歩的なエラー確認の欠如に警告メッセージを出せる。

[[nodiscard]]属性は、クラスとenumにも付与することができる。

class [[nodiscard]] X { } ;
enum class [[nodiscard]] Y { } ;

[[nodiscard]]が付与されたクラスかenumが戻り値の型である関数は[[nodiscard]]が付与された扱いとなる。

class [[nodiscard]] X { } ;

X f() { return X{} ; } 

int main()
{
    // 警告、戻り値が無視されている
    f() ;
}

機能テストマクロは__has_cpp_attribute(nodiscard), 値は201603。

[[maybe_unused]]属性

[[maybe_unused]]属性は名前やエンティティが意図的に使われないことを示すのに使える。

現実のC++のコードでは、宣言されているのにソースコードだけを考慮するとどこからも使われていないように見える名前やエンティティが存在する。

void do_something( int *, int * ) ;

void f()
{
    int x[5] ;
    char reserved[1024] = { } ;
    int y[5] ;

    do_something( x, y ) ;
}

ここではreservedという名前はどこからも使われていない。一見すると不必要な名前に見える。優秀なC++コンパイラーはこのようなどこからも使われていない名前に対して「どこからも使われていない」という警告メッセージを出す。

しかし、コンパイラーから見えているソースコードがプログラムのすべてではない。さまざまな理由でreservedのような一見使われていない変数が必要になる。

たとえば、reservedはスタック破壊を検出するための領域かもしれない。プログラムはC++以外の言語で書かれたコードとリンクしていて、そこで使われるのかもしれない。あるいはOSや外部デバイスが読み書きするメモリーとして確保しているのかもしれない。

どのような理由にせよ、名前やエンティティが一見使われていないように見えるが存在が必要であるという意味を表すのに、[[maybe_unused]]属性を使うことができる。これにより、C++コンパイラーの「未使用の名前」という警告メッセージを抑制できる。

[[maybe_unused]] char reserved[1024] ;

[[maybe_unused]]属性を適用できる名前とエンティティの宣言は、クラス、typedef名、変数、非staticデータメンバー、関数、enum, enumeratorだ。

// クラス
class [[maybe_unused]] class_name
{
// 非staticデータメンバー
    [[maybe_unused]] int non_static_data_member ;

} ;

// typedef名
// どちらでもよい
[[maybe_unused]] typedef int typedef_name1 ;
typedef int typedef_name2 [[maybe_unused]] ;

// エイリアス宣言によるtypedef名
using typedef_name3 [[maybe_unused]] = int ;

// 変数
// どちらでもよい
[[maybe_unused]] int variable_name1{};
int variable_name2 [[maybe_unused]] { } ;

// 関数
// メンバー関数も同じ文法
// どちらでもよい
[[maybe_unused]] void function_name1() { }
void function_name2 [[maybe_unused]] () { }

enum [[maybe_unused]] enum_name
{
// enumerator
    enumerator_name [[maybe_unused]] = 0
} ;

機能テストマクロは__has_cpp_attribute(maybe_unused), 値は201603

演算子のオペランドの評価順序の固定

C++17では演算子のオペランドの評価順序が固定された。

以下の式は、a, bの順番に評価されることが規格上保証される。@=@には文法上許される任意の演算子が入る(+=, -=など)。

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

つまり、

int* f() ;
int g() ;

int main()
{
   f()[g()] ; 
}

と書いた場合、関数fがまず先に呼び出されて、次に関数gが呼び出されることが保証される。

関数呼び出しの実引数のオペランドb1, b2, b3の評価順序は未規定のままだ。

これにより、既存の未定義の動作となっていたコードの挙動が定まる。

constexpr if文 : コンパイル時条件分岐

constexpr if文はコンパイル時の条件分岐ができる機能だ。

constexpr if文は、通常のif文をif constexprで置き換える。

// if文
if ( expression )
    statement ;

// constexpr if文
if constexpr ( expression )
    statement ;

constexpr if文という名前だが、実際に記述するときはif constexprだ。

コンパイル時の条件分岐とは何を意味するのか。以下はconstexpr if行わないものの一覧だ。

コンパイル時の条件分岐の機能を理解するには、まずC++の既存の条件分岐について理解する必要がある。

実行時の条件分岐

通常の実行時の条件分岐は、実行時の値を取り、実行時に条件分岐を行う。

void f( bool runtime_value )
{
    if ( runtime_value )
        do_true_thing() ;
    else
        do_false_thing() ;
}

この場合、runtime_valuetrueの場合は関数do_true_thingが呼ばれ、falseの場合は関数do_false_thingが呼ばれる。

実行時の条件分岐の条件には、コンパイル時定数を指定できる。

if ( true )
    do_true_thing() ;
else
    do_false_thing() ;

この場合、賢いコンパイラーは以下のように処理を最適化するかもしれない。

do_true_thing() ;

なぜならば、条件は常にtrueだからだ。このような最適化は実行時の条件分岐でもコンパイル時に行える。コンパイル時の条件分岐はこのような最適化が目的ではない。

もう一度コード例に戻ろう。今度は完全なコードを見てみよう。

// do_true_thingの宣言
void do_true_thing() ;

// do_false_thingの宣言は存在しない

void f( bool runtime_value )
{
    if ( true )
        do_true_thing() ;
    else
        do_false_thing() ; // エラー
}

このコードはエラーになる。その理由は、do_false_thingという名前が宣言されていないからだ。C++コンパイラーは、コンパイル時にコードを以下の形に変形することで最適化することはできるが、

void do_true_thing() ;

void f( bool runtime_value )
{
    do_true_thing() ;
}

最適化の結果失われたものも、依然としてコンパイル時にコードとして検証はされる。コードとして検証されるということは、コードとして誤りがあればエラーとなる。名前do_false_thingは宣言されていないのでエラーとなる。

プリプロセス時の条件分岐

C++がC言語から受け継いだCプリプロセッサーには、プリプロセス時の条件分岐の機能がある。

// do_true_thingの宣言
void do_true_thing() ;

// do_false_thingの宣言は存在しない

void f( bool runtime_value )
{

#if true
    do_true_thing() ;
#else
    do_false_thing() ;
#endif
}

このコードは、プリプロセスの結果、以下のように変換される。

void do_true_thing() ;

void f( bool runtime_value )
{
    do_true_thing() ;
}

この結果、プリプロセス時の条件分岐では、選択されない分岐はコンパイルされないので、コンパイルエラーになるコードも書くことができる。

プリプロセス時の条件分岐は、条件が整数とかbool型のリテラルか、リテラルに比較演算子を適用した結果ではうまくいく。しかし、プリプロセス時とはコンパイル時ではないので、コンパイル時計算はできない。

constexpr int f()
{
    return 1 ;
}

void do_true_thing() ;

int main()
{
// エラー
// 名前fはプリプロセッサーマクロではない
#if f()
    do_true_thing() ;
#else
    do_false_thing() ;
#endif
}

コンパイル時の条件分岐

コンパイル時の条件分岐とは、分岐の条件にコンパイル時計算の結果を使い、かつ、選択されない分岐にコンパイルエラーが含まれていても、使われないのでコンパイルエラーにはならない条件分岐のことだ。

たとえば、std::distanceという標準ライブラリを実装してみよう。std::distance(first, last)は、イテレーターfirstlastの距離を返す。

template < typename Iterator >
constexpr typename std::iterator_traits<Iterator>::difference_type
distance( Iterator first, Iterator last )
{
    return last - first ;
}

残念ながら、この実装はIteratorがランダムアクセスイテレーターの場合にしか動かない。入力イテレーターに対応させるには、イテレーターを1つずつインクリメントしてlastと等しいかどうか比較する実装が必要になる。

template < typename Iterator >
constexpr typename std::iterator_traits<Iterator>::difference_type
distance( Iterator first, Iterator last )
{
    typename std::iterator_traits<Iterator>::difference_type n = 0 ;

    while ( first != last )
    {
        ++n ;
        ++first ;
    }

    return n ;
}

残念ながら、この実装はIteratorにランダムアクセスイテレーターを渡したときに効率が悪い。

ここで必要な実装は、Iteratorがランダムアクセスイテレーターならばlast - firstを使い、そうでなければ地道にインクリメントする遅い実装を使うことだ。Iteratorがランダムアクセスイテレーターかどうかは、以下のコードを使えば、is_random_access_iterator<iterator>で確認できる。

template < typename Iterator >
constexpr bool is_random_access_iterator =
    std::is_same_v<
        typename std::iterator_traits< 
            std::decay_t<Iterator> 
        >::iterator_category,
        std::random_access_iterator_tag > ;

すると、distanceは以下のように書けるのではないか。

// ランダムアクセスイテレーターかどうかを判定するコード
template < typename Iterator >
constexpr bool is_random_access_iterator =
    std::is_same_v<
        typename std::iterator_traits< 
            std::decay_t<Iterator>
        >::iterator_category,
        std::random_access_iterator_tag > ;

// distance
template < typename Iterator >
constexpr typename std::iterator_traits<Iterator>::difference_type
distance( Iterator first, Iterator last )
{
    // ランダムアクセスイテレーターかどうか確認する
    if ( is_random_access_iterator<Iterator> )
    {// ランダムアクセスイテレーターなので速い方法を使う
        return last - first ;
    }
    else
    { // ランダムアクセスイテレーターではないので遅い方法を使う
        typename std::iterator_traits<Iterator>::difference_type n = 0 ;

        while ( first != last )
        {
            ++n ;
            ++first ;
        }

        return n ;
    }
}

残念ながら、このコードは動かない。ランダムアクセスイテレーターではないイテレーターを渡すと、last - firstというコードがコンパイルされるので、コンパイルエラーになる。コンパイラーは、

if ( is_random_access_iterator<Iterator> )

という部分について、is_random_access_iterator<Iterator>の値はコンパイル時に計算できるので、最終的なコード生成の結果としては、if (true)if (false)になると判断できる。したがってコンパイラーは選択されない分岐のコード生成を行わないことはできる。しかしコンパイルはするので、コンパイルエラーになる。

constexpr ifを使うと、選択されない部分の分岐はコンパイルエラーであってもコンパイルエラーとはならなくなる。

// distance
template < typename Iterator >
constexpr typename std::iterator_traits<Iterator>::difference_type
distance( Iterator first, Iterator last )
{
    // ランダムアクセスイテレーターかどうか確認する
    if constexpr ( is_random_access_iterator<Iterator> )
    {// ランダムアクセスイテレーターなので速い方法を使う
        return last - first ;
    }
    else
    { // ランダムアクセスイテレーターではないので遅い方法を使う
        typename std::iterator_traits<Iterator>::difference_type n = 0 ;

        while ( first != last )
        {
            ++n ;
            ++first ;
        }

        return n ;
    }
}

超上級者向け解説

constexpr ifは、実はコンパイル時条件分岐ではない。テンプレートの実体化時に、選択されないブランチのテンプレートの実体化の抑制を行う機能だ。

constexpr ifによって選択されない文はdiscarded statementとなる。discarded statementはテンプレートの実体化の際に実体化されなくなる。

struct X
{
   int get() { return 0 ; } 
} ;

template < typename T >
int f(T x)
{
    if constexpr ( std::is_same_v< std::decay_t<T>, X > )
        return x.get() ;
    else
        return x ;

}

int main()
{
    X x ;
    f( x ) ; // return x.get() 
    f( 0 ) ; // return x
}

f(x)では、return xdiscarded statementとなるため実体化されない。Xint型に暗黙に変換できないが問題がなくなる。f(0)ではreturn x.get()discarded statementとなるため実体化されない。int型にはメンバー関数getはないが問題はなくなる。

discarded statementは実体化されないだけで、もちろんテンプレートのエンティティの一部だ。discarded statementがテンプレートのコードとして文法的、意味的に正しくない場合は、もちろんコンパイルエラーとなる。

template < typename T >
void f( T x )
{
    // エラー、名前gは宣言されていない
    if constexpr ( false )
        g() ; 

    // エラー、文法違反
    if constexpr ( false )
        !#$%^&*()_+ ;
}

何度も説明しているように、constexpr ifはテンプレートの実体化を条件付きで抑制するだけだ。条件付きコンパイルではない。

template < typename T >
void f()
{
    if constexpr ( std::is_same_v<T, int> )
    {
        // 常にコンパイルエラー
        static_assert( false ) ;
    }
}

このコードは常にコンパイルエラーになる。なぜならば、static_assert( false )はテンプレートに依存しておらず、テンプレートの宣言を解釈するときに、依存名ではないから、そのまま解釈される。

このようなことをしたければ、最初からstatic_assertのオペランドに式を書けばよい。

template < typename T >
void f()
{
    static_assert( std::is_same_v<T, int> ) ;

    if constexpr ( std::is_same_v<T, int> )
    {
    }
}

もし、どうしてもconstexpr文の条件に合うときにだけstatic_assertが使いたい場合もある。これは、constexpr ifをネストしたりしていて、その内容を全部static_assertに書くのが冗長な場合だ。

template < typename T >
void f()
{
    if constexpr ( E1 )
        if constexpr ( E2 )
            if constexpr ( E3 )
            {
                // E1 && E2 && E3のときにコンパイルエラーにしたい
                // 実際には常にコンパイルエラー
                static_assert( false ) ;
            }
}

現実には、E1, E2, E3は複雑な式なので、static_assert( E1 && E2 && E3 )と書くのは冗長だ。同じ内容を二度書くのは間違いの元だ。

このような場合、static_assertのオペランドをテンプレート引数に依存するようにすると、constexpr ifの条件に合うときにだけ発動するstatic_assertが書ける。

template  < typename ... >
bool false_v = false ;

template < typename T >
void f()
{
    if constexpr ( E1 )
        if constexpr ( E2 )
            if constexpr ( E3 )
            {
                static_assert( false_v<T> ) ;
            }
}

このようにfalse_vを使うことで、static_assertをテンプレート引数Tに依存させる。その結果、static_assertの発動をテンプレートの実体化まで遅延させることができる。

constexpr ifは非テンプレートコードでも書くことができるが、その場合は普通のif文と同じだ。

constexpr ifでは解決できない問題

constexpr ifは条件付きコンパイルではなく、条件付きテンプレート実体化の抑制なので、最初の問題の解決には使えない。たとえば以下のコードはエラーになる。

// do_true_thingの宣言
void do_true_thing() ;

// do_false_thingの宣言は存在しない

void f( bool runtime_value )
{
    if constexpr ( true )
        do_true_thing() ;
    else
        do_false_thing() ; // エラー
}

理由は、名前do_false_thingは非依存名なのでテンプレートの宣言時に解決されるからだ。

constexpr ifで解決できる問題

constexpr ifは依存名が関わる場合で、テンプレートの実体化がエラーになる場合に、実体化を抑制させることができる。

たとえば、特定の型に対して特別な操作をしたい場合

struct X
{
    int get_value() ;
} ;

template < typename T >
void f(T t)
{
    
    int value{} ;

    // Tの型がXならば特別な処理を行いたい
    if constexpr ( std::is_same<T, X>{} )
    {
        value = t.get_value() ;
    }
    else
    {
        value = static_cast<int>(t) ;
    }
}

もしconstexpr ifがなければ、Tの型がXではないときもt.get_value()という式が実体化され、エラーとなる。

再帰的なテンプレートの特殊化をやめさせたいとき

// factorial<N>はNの階乗を返す
template < std::size_t I  >
constexpr std::size_t factorial()
{
    if constexpr ( I == 1 )
    { return 1 ; }
    else
    { return I * factorial<I-1>() ; }
}

もしconstexpr ifがなければ、factorial<N-1>が永遠に実体化されコンパイル時ループが停止しない。

機能テストマクロは__cpp_if_constexpr, 値は201606。

初期化文付き条件文

C++17では、条件文に初期化文を記述できるようになった。

if ( int x = 1 ; x )
     /*...*/ ;

switch( int x = 1 ; x )
{
    case 1 :
        /*... */;
}

これは、以下のコードと同じ意味になる。

{
    int x = 1 ;
    if ( x ) ;
}

{
    int x = 1 ;
    switch( x )
    {
        case 1 : ;
    }
}

なぜこのような機能が追加されたかというと、変数を宣言し、if文の条件に変数を使い、if文を実行後は変数を使用しない、というパターンは現実のコードで頻出するからだ。

void * ptr = std::malloc(10) ;
if ( ptr != nullptr )
{
    // 処理
    std::free(ptr) ;
}
// これ以降ptrは使わない

FILE * file = std::fopen("text.txt", "r") ;
if ( file != nullptr )
{
    // 処理
    std::fclose( file ) ;
}
// これ以降fileは使わない

auto int_ptr = std::make_unique<int>(42) ;
if ( ptr )
{
    // 処理
}
// これ以降int_ptrは使わない

上記のコードには問題がある。これ以降変数は使わないが、変数自体は使えるからだ。

auto ptr = std::make_unique<int>(42) ;
if ( ptr )
{
    // 処理
}
// これ以降ptrは使わない

// でも使える
int value = *ptr ;

変数を使えないようにするには、ブロックスコープで囲むことで、変数をスコープから外してやればよい。

{
    auto int_ptr = std::make_unique<int>(42) ;
    if ( ptr )
    {
        // 処理
    }
    // ptrは破棄される
}
// これ以降ptrは使わないし使えない

このようなパターンは頻出するので、初期化文付きの条件文が追加された。

if ( auto ptr = std::make_unique<int>(42) ; ptr )
{
    // 処理
}

クラステンプレートのコンストラクターからの実引数推定

C++17ではクラステンプレートのコンストラクターの実引数からテンプレート実引数の推定が行えるようになった。

template < typename T >
struct X
{
    X( T t ) { }
} ;

int main()
{
    X x1(0) ; // X<int>
    X x2(0.0) ; // X<double>
    X x3("hello") ; // X<char const *>
}

これは関数テンプレートが実引数からテンプレート実引数の推定が行えるのと同じだ。

template < typename T >
void f( T t ) { }

int main()
{
    f( 0 ) ; // f<int>
    f( 0.0 ) ; // f<double>
    f( "hello" ) ; // f<char const *>
}

推定ガイド

クラステンプレートのコンストラクターからの実引数は便利だが、クラスのコンストラクターはクラステンプレートのテンプレートパラメーターに一致しない場合もある。そのような場合はそのままでは実引数推定ができない。

// コンテナー風のクラス
template < typename T >
class Container
{
    std::vector<T> c ;
public :
    // 初期化にイテレーターのペアを取る
    // IteratorはTではない
    // Tは推定できない
    template < typename Iterator >
    Container( Iterator first, Iterator last )
        : c( first, last )
    { }
} ;


int main()
{
    int a[] = { 1,2,3,4,5 } ;

    // エラー
    // Tを推定できない
    Container c( std::begin(a), std::end(a) ) ;
}

このため、C++17には推定ガイドという機能が提供されている。

テンプレート名( 引数リスト ) -> テンプレートid ;

これを使うと、以下のように書ける。

template < typename Iterator >
Container( Iterator, Iterator )
-> Container< typename std::iterator_traits< Iterator >::value_type > ;

C++コンパイラーはこの推定ガイドを使って、Container<T>::Container(Iterator, Iterator)からは、Tstd::iterator_traits<Iterator>::value_typeとして推定すればいいのだと判断できる。

たとえば、初期化リストに対応するには以下のように書く。

template < typename T >
class Container
{
    std::vector<T> c ;
public :

    Container( std::initializer_list<T> init )
        : c( init )
    { }
} ;


template < typename T >
Container( std::initializer_list<T> ) -> Container<T> ;


int main()
{
    Container c = { 1,2,3,4,5 } ;
}

C++コンパイラーはこの推定ガイドから、Container<T>::Container( std::initializer_list<T> )の場合はTTとして推定すればよいことがわかる。

機能テストマクロは__cpp_deduction_guides, 値は201606。

autoによる非型テンプレートパラメーターの宣言

C++17では非型テンプレートパラメーターの宣言にautoを使うことができるようになった。

template < auto x >
struct X { } ;

void f() { }

int main()
{
    X<0> x1 ;
    X<0l> x2 ;
    X<&f> x3 ;
}

これはC++14までであれば、以下のように書かなければならなかった。

template < typename T, T x >
struct X { } ;

void f() { }

int main()
{
    X<int, 0> x1 ;
    X<long, 0l> x2 ;
    X<void(*)(), &f> x3 ;
}

機能テストマクロは__cpp_template_auto, 値は201606。

using属性名前空間

C++17では、属性名前空間にusingディレクティブのような記述ができるようになった。


// [[extension::foo, extension::bar]]と同じ
[[ using extension : foo, bar ]] int x ;

属性トークンには、属性名前空間を付けることができる。これにより、独自拡張の属性トークンの名前の衝突を避けることができる。

たとえば、あるC++コンパイラーには独自拡張としてfoo, barという属性トークンがあり、別のC++コンパイラーも同じく独自拡張としてfoo, barという属性トークンを持っているが、それぞれ意味が違っている場合、コードの意味も違ってしまう。

[[ foo, bar ]] int x ;

このため、C++には属性名前空間という文法が用意されている。注意深いC++コンパイラーは独自拡張の属性トークンには属性名前空間を設定していることだろう。

[[ extension::foo, extension::bar ]] int x ;

問題は、これをいちいち記述するのは面倒だということだ。

C++17では、using属性名前空間という機能により、usingディレクティブのような名前空間の省略が可能になった。文法はusingディレクティブと似ていて、属性の中でusing name : ...と書くことで、コロンに続く属性トークンに、属性名前空間nameを付けたものと同じ効果が得られる。

非標準属性の無視

C++17では、非標準の属性トークンは無視される。

// OK、無視される
[[ wefapiaofeaofjaopfij ]] int x ;

属性はC++コンパイラーによる独自拡張をC++の規格に準拠する形で穏便に追加するための機能だ。その属性のためにコンパイルエラーになった場合、けっきょくCプリプロセッサーを使うか、わずらわしさから独自の文法が使われてしまう。そのためこの機能は必須だ。

構造化束縛

C++17で追加された構造化束縛は多値を分解して受け取るための変数宣言の文法だ。

int main()
{
    int a[] = { 1,2,3 } ;
    auto [b,c,d] = a ;

    // b == 1
    // c == 2
    // d == 3
}

C++では、さまざまな方法で多値を扱うことができる。たとえば配列、クラス、tuple, pairだ。

int a[] = { 1,2,3 } ;
struct B
{
    int a ;
    double b ;
    std::string c ;
} ;

B b{ 1, 2.0, "hello" } ;

std::tuple< int, double, std::string > c { 1, 2.0, "hello" } ;

std::pair< int, int > d{ 1, 2 } ;

C++の関数は配列以外の多値を返すことができる。

std::tuple< int, double, std::string > f()
{
    return { 1, 2.0, "hello" } ;
}

多値を受け取るには、これまでは多値を固まりとして受け取るか、ライブラリで分解して受け取るしかなかった。

多値を固まりで受け取るには以下のように書く。

std::tuple< int, double, std::string > f()
{
    return { 1, 2.0, "hello" } ;
}

int main()
{
    auto result = f() ;
    
    std::cout << std::get<0>(result) << '\n' 
        << std::get<1>(result) << '\n'
        << std::get<2>(result) << std::endl ;
}

多値をライブラリで受け取るには以下のように書く。

std::tuple< int, double, std::string > f()
{
    return { 1, 2.0, "hello" } ;
}

int main()
{
    int a ;
    double b ;
    std::string c ;

    std::tie( a, b, c ) = f() ;
    
    std::cout << a << '\n' 
        << b << '\n'
        << c << std::endl ;
}

構造化束縛を使うと、以下のように書ける。

std::tuple< int, double, std::string > f()
{
    return { 1, 2.0, "hello" } ;
}

int main()
{
    auto [a, b, c] = f() ;
    
    std::cout << a << '\n' 
        << b << '\n'
        << c << std::endl ;
}

変数の型はそれぞれ対応する多値の型になる。この場合、a, b, cはそれぞれint, double, std::string型になる。

tupleだけではなく、pairも使える。

int main()
{
    std::pair<int, int> p( 1, 2 ) ;

    auto [a,b] = p ;

    // aはint型、値は1
    // bはint型、値は2
}

構造化束縛はif文とswitch文、for文でも使える。

int main()
{
    int expr[] = {1,2,3} ;

    if ( auto[a,b,c] = expr ; a )
    { }
    switch( auto[a,b,c] = expr ; a )
    { }
    for ( auto[a,b,c] = expr ; false ; ) 
    { }
}

構造化束縛はrange-based for文にも使える。

int main()
{
    std::map< std::string, std::string > translation_table
    {
        {"dog", "犬"},
        {"cat", "猫"},
        {"answer", "42"} 
    } ;
    
    for ( auto [key, value] : translation_table )
    {
        std::cout<<
            "key="<< key <<
            ", value=" << value << '\n' ;
    }
}

これは、mapの要素型std::pair<const std::string, std::string>を構造化束縛[key, value]で受けている。

構造化束縛は配列にも使える。

int main()
{
    int values[] = {1,2,3} ;
    auto [a,b,c] = values ;
}

構造化束縛はクラスにも使える。

struct Values
{
    int a ;
    double d ;
    std::string c ;
} ;

int main()
{
    Values values{ 1, 2.0, "hello" } ;

    auto [a,b,c] = values ;
}

構造化束縛でクラスを使う場合は、非staticデータメンバーはすべて1つのクラスのpublicなメンバーでなければならない。

構造化束縛はconstexprにはできない。

int main()
{
    constexpr int expr[] = { 1,2 } ;

    // エラー
    constexpr auto [a,b] = expr ;
}

超上級者向け解説

構造化束縛は、変数の宣言のうち、構造化束縛宣言(structured binding declaration)に分類される文法で記述する。構造化束縛宣言となる宣言は、単純宣言(simple-declaration)とfor-range宣言(for-range-declaration)のうち、[識別子リスト]があるものだ。

単純宣言:
    属性 auto CV修飾子(省略可) リファレンス修飾子(省略可)
    [ 識別子リスト ] 初期化子 ;

for-range宣言:
    属性 auto CV修飾子(省略可) リファレンス修飾子(省略可)
    [ 識別子リスト ] ;

識別子リスト:
    コンマで区切られた識別子

初期化子:
    = 式
    { 式 }
    ( 式 )

以下は単純宣言のコード例だ。

int main()
{
    int e1[] = {1,2,3} ;
    struct { int a,b,c ; } e2{1,2,3} ;
    auto e3 = std::make_tuple(1,2,3) ;
    
    // "= 式"の例
    auto [a,b,c] = e1 ;
    auto [d,e,f] = e2 ;
    auto [g,h,i] = e3 ;
    
    // "{式}", "(式)"の例
    auto [j,k,l]{e1} ;
    auto [m,n,o](e1) ;

    // CV修飾子とリファレンス修飾子を使う例
    auto const & [p,q,r] = e1 ;
}

以下はfor-range宣言の例だ。

int main()
{
    std::pair<int, int> pairs[] = { {1,2}, {3,4}, {5,6} } ;
    
    for ( auto [a, b] : pairs )
    {
        std::cout << a << ", " << b << '\n' ;
    }
}

構造化束縛宣言の仕様

構造化束縛の構造化束縛宣言は以下のように解釈される。

構造化束縛宣言によって宣言される変数の数は、初期化子の多値の数と一致していなければならない。

int main()
{
    // 2個の値を持つ
    int expr[] = {1,2} ;

    // エラー、変数が少なすぎる
    auto[a] = expr ; 
    // エラー、変数が多すぎる
    auto[b,c,d] = expr ;
}

構造化束縛宣言で宣言されるそれぞれの変数名について、記述されたとおりの属性、CV修飾子、リファレンス修飾子の変数が宣言される。

初期化子の型が配列の場合

初期化子が配列の場合、それぞれの変数はそれぞれの配列の要素で初期化される。

リファレンス修飾子がない場合、それぞれの変数はコピー初期化される。

int main()
{
    int expr[3] = {1,2,3} ;
    auto [a,b,c] = expr ;
}

これは、以下と同じ意味になる。

int main()
{

    int expr[3] = {1,2,3} ;

    int a = expr[0] ;
    int b = expr[1] ;
    int c = expr[2] ;
}

リファレンス修飾子がある場合、変数はリファレンスとなる。

int main()
{
    int expr[3] = {1,2,3} ;
    auto & [a,b,c] = expr ;
    auto && [d,e,f] = expr ;
}

これは、以下と同じ意味になる。

int main()
{
    int expr[3] = {1,2,3} ;

    int & a = expr[0] ;
    int & b = expr[1] ;
    int & c = expr[2] ;

    int && d = expr[0] ;
    int && e = expr[1] ;
    int && f = expr[2] ;
}

もし、変数の型が配列の場合、配列の要素はそれぞれ対応する配列の要素で初期化される。

int main()
{
    int expr[][2] = {{1,2},{1,2}} ;
    auto [a,b] = expr ;
}

これは、以下と同じ意味になる。

int main()
{
    int expr[][2] = {{1,2},{1,2}} ;

    int a[2] = { expr[0][0], expr[0][1] } ;
    int b[2] = { expr[1][0], expr[1][1] } ;    
}

初期化子の型が配列ではなく、std::tuple_size<E>が完全形の名前である場合

構造化束縛宣言の初期化子の型Eが配列ではない場合で、std::tuple_size<E>が完全形の名前である場合、 構造化束縛宣言の初期化子の型をE, その値をeとする。構造化束縛宣言で宣言される1つ目の変数を0, 2つ目の変数を1, ..., とインクリメントされていくインデックスをiとする。

std::tuple_size<E>::valueは整数のコンパイル時定数式で、その値は初期化子の値の数でなければならない。

int main()
{
    // std::tuple< int, int, int >
    auto e = std::make_tuple( 1, 2, 3 ) ;
    auto [a,b,c] = e ;

    // std::tuple_size<decltype(e)>::sizeは3
}

それぞれの値を取得するために、非修飾名getが型Eのクラススコープから探される。getが見つかった場合、それぞれの変数の初期化子はe.get<i>()となる。

auto [a,b,c] = e ;

という構造化束縛宣言は、以下の意味になる。

type a = e.get<0>() ;
type b = e.get<1>() ;
type c = e.get<2>() ;

そのようなgetの宣言が見つからない場合、初期化子はget<i>(e)となる。この場合、getは連想名前空間から探される。通常の非修飾名前検索は行われない。

// ただし通常の非修飾名前検索は行われない
type a = get<0>(e) ;
type b = get<1>(e) ;
type c = get<2>(e) ;

構造化束縛宣言で宣言される変数の型は以下のように決定される。

変数の型typeは"std::tuple_element<i, E>::type"となる。

std::tuple_element<0, E>::type a = get<0>(e) ;
std::tuple_element<1, E>::type b = get<1>(e) ;
std::tuple_element<2, E>::type c = get<2>(e) ;

以下のコードは、

int main()
{
    auto e = std::make_tuple( 1, 2, 3 ) ;
    auto [a,b,c] = e ;
}

以下とほぼ同等の意味になる。

int main()
{
    auto e = std::make_tuple( 1, 2, 3 ) ;
    
    using E = decltype(e) ;

    std::tuple_element<0, E>::type a = std::get<0>(e) ;
    std::tuple_element<1, E>::type b = std::get<1>(e) ;
    std::tuple_element<2, E>::type c = std::get<2>(e) ;
}

以下のコードは、

int main()
{
    auto e = std::make_tuple( 1, 2, 3 ) ;
    auto && [a,b,c] = std::move(e) ;
}

以下のような意味になる。

int main()
{
    auto e = std::make_tuple( 1, 2, 3 ) ;
    
    using E = decltype(e) ;

    std::tuple_element<0, E>::type && a = std::get<0>(std::move(e)) ;
    std::tuple_element<1, E>::type && b = std::get<1>(std::move(e)) ;
    std::tuple_element<2, E>::type && c = std::get<2>(std::move(e)) ;
}

上記以外の場合

上記以外の場合、構造化束縛宣言の初期化子の型Eはクラス型で、すべての非staticデータメンバーはpublicの直接のメンバーであるか、あるいは単一の曖昧ではないpublic基本クラスのメンバーである必要がある。Eに匿名unionメンバーがあってはならない。

以下は型Eとして適切なクラスの例である。

struct A
{
    int a, b, c ;
} ;

struct B : A { } ;

以下は型Eとして不適切なクラスの例である。

// public以外の非staticデータメンバーがある
struct A
{
public :
    int a ;
private :
    int b ;
} ;



struct B
{
    int a ;
} ;
// クラスにも基本クラスにも非staticデータメンバーがある
struct C : B
{
    int b ;
} ;

// 匿名unionメンバーがある
struct D
{
    union
    {
        int i ;
        double d ;
    }
} ;

Eの非staticデータメンバーは宣言された順番で多値として認識される。

以下のコードは、

int main()
{
    struct { int x, y, z ; } e{1,2,3} ;

    auto [a,b,c] = e ;
}

以下のコードと意味的に等しい。

int main()
{
    struct { int x, y, z ; } e{1,2,3} ;

    int a = e.x ;
    int b = e.y ;
    int c = e.z ;
}

構造化束縛はビットフィールドに対応している。

struct S
{
    int x : 2 ;
    int y : 4 ;
} ;

int main()
{
    S e{1,3} ;
    auto [a,b] = e ;
}

機能テストマクロは__cpp_structured_bindings, 値は201606。

inline変数

C++17では変数にinlineキーワードを指定できるようになった。

inline int variable ;

このような変数をinline変数と呼ぶ。その意味はinline関数と同じだ。

inlineの歴史的な意味

今は昔、本書執筆から30年以上は昔に、inlineキーワードがC++に追加された。

inlineの現在の意味は誤解されている。

inline関数の意味は、「関数を強制的にインライン展開させるための機能」ではない

大事なことなのでもう一度書くが、inline関数の意味は、「関数を強制的にインライン展開させるための機能」ではない

確かに、かつてinline関数の意味は、関数を強制的にインライン展開させるための機能だった。

関数のインライン展開とは、たとえば以下のようなコードがあったとき、

int min( int a, int b )
{ return a < b ? a : b ; }

int main()
{
    int a, b ;
    std::cin >> a >> b ;

    // aとbのうち小さい方を選ぶ
    int value = min( a, b ) ;
}

この関数minは十分に小さく、関数呼び出しのコストは無視できないオーバーヘッドになるため、以下のような最適化が考えられる。

int main()
{
    int a, b ;
    std::cin >> a >> b ;

    int value = a < b ? a : b ;
}

このように関数の中身を展開することを、関数のインライン展開という。

人間が関数のインライン展開を手で行うのは面倒だ。それにコードが読みにくい。"min(a,b)"と"a<b?a:b"のどちらが読みやすいだろうか。

幸い、C++コンパイラーはインライン展開を自動的に行えるので人間が苦労する必要はない。

インライン展開は万能の最適化ではない。インライン展開をすると逆に遅くなる場合もある。

たとえば、ある関数をコンパイルした結果のコードサイズが1Kバイトあったとして、その関数を呼んでいる箇所がプログラム中に1000件ある場合、プログラム全体のサイズは1Mバイト増える。コードサイズが増えるということは、CPUのキャッシュを圧迫する。

たとえば、ある関数の実行時間が関数呼び出しの実行時間に比べて桁違いに長いとき、関数呼び出しのコストを削減するのは意味がない。

したがって関数のインライン展開という最適化を適用すべきかどうかを決定するには、関数のコードサイズが十分に小さいとき、関数の実行時間が十分に短いとき、タイトなループの中など、さまざまな条件を考慮しなければならない。

昔のコンパイラー技術が未熟だった時代のC++コンパイラーは関数をインライン展開するべきかどうかの判断ができなかった。そのためinlineキーワードが追加された。インライン展開してほしい関数をinline関数にすることで、コンパイラーはその関数がインライン展開するべき関数だと認識する。

現代のinlineの意味

現代では、コンパイラー技術の発展によりC++コンパイラーは十分に賢くなったので、関数をインライン展開させる目的でinlineキーワードを使う必要はない。実際、現代のC++コンパイラーではinlineキーワードはインライン展開を強制しない。関数をインライン展開すべきかどうかはコンパイラーが判断できる。

inlineキーワードにはインライン展開以外に、もう1つの意味がある。ODR(One Definition Rule、定義は1つの原則)の回避だ。

C++では、定義はプログラム中に1つしか書くことができない。

void f() ; // OK、宣言
void f() ; // OK、再宣言

void f() { } // OK、定義

void f() { } // エラー、再定義

通常は、関数を使う場合には宣言だけを書いて使う。定義はどこか1つの翻訳単位に書いておけばよい。

// f.h

void f() ;

// f.cpp

void f() { }

// main.cpp

#include "f.h"

int main()
{
    f() ;
}

しかし、関数のインライン展開をするには、コンパイラーの実装上の都合で、関数の定義が同じ翻訳単位になければならない。

inline void f() ;

int main()
{
    // エラー、定義がない
    f() ; 
}

しかし、翻訳単位ごとに定義すると、定義が重複してODRに違反する。

C++ではこの問題を解決するために、inline関数は定義が同一であれば、複数の翻訳単位で定義されてもよいことにしている。つまりODRに違反しない。

// a.cpp

inline void f() { }

void a()
{
    f() ;
}

// b.cpp

// OK、inline関数
inline void f() { }

void b()
{
    f() ;
}

これは例のために同一のinline関数を直接記述しているが、inline関数は定義の同一性を保証させるため、通常はヘッダーファイルに書いて#includeして使う。

inline変数の意味

inline変数は、ODRに違反せず変数の定義の重複を認める。同じ名前のinline変数は同じ変数を指す。

// a.cpp

inline int data ;

void a() { ++data ; }

// b.cpp

inline int data ;

void b() { ++data ; }

// main.cpp

inline int data ;

int main()
{
    a() ;
    b() ;

    data ; // 2
}

この例で関数a, bの中の変数dataは同じ変数を指している。変数datastaticストレージ上に構築された変数なのでプログラムの開始時にゼロで初期化される。2回インクリメントされるので値は2となる。

これにより、クラスのstaticデータメンバーの定義を書かなくてすむようになる。

C++17以前のC++では、以下のように書かなければならなかったが、

// S.h

struct S
{
    static int data ;
} ;

// S.cpp

int S::data ;

C++17では、以下のように書けばよい。

// S.h

struct S
{
    inline static int data ;
} ;

S.cppに変数S::dataの定義を書く必要はない。

機能テストマクロは__cpp_inline_variables, 値は201606。

可変長using宣言

この機能は超上級者向けだ。

C++17ではusing宣言をコンマで区切ることができるようになった。

int x, y ;

int main()
{
    using ::x, ::y ;
}

これは、C++14で

using ::x ;
using ::y ;

と書くのと等しい。

C++17では、using宣言でパック展開ができるようになった。この機能に正式な名前は付いていないが、可変長using宣言(Variadic using declaration)と呼ぶのがわかりやすい。

template < typename ... Types >
struct S : Types ...
{
    using Types::operator() ... ;
    void operator () ( long ) { }
} ;


struct A
{
    void operator () ( int ) { }
} ;

struct B
{
    void operator () ( double ) { }
} ;

int main()
{
    S<A, B> s ;
    s(0) ; // A::operator()
    s(0L) ; // S::operator()
    s(0.0) ; // B::operator()
}

機能テストマクロは__cpp_variadic_using, 値は201611。

std::byte : バイトを表現する型

C++17では、バイトを表現する型が入った。ライブラリでもあるのだがコア言語で特別な型として扱われている。

バイトとはC++のメモリーモデルにおけるストレージの単位で、C++においてユニークなアドレスが付与される最小単位だ。C++の規格はいまだに1バイトが具体的に何ビットであるのかを規定していない。これは過去にバイトのサイズが8ビットではないアーキテクチャーが存在したためだ。

バイトのビット数は<climits>で定義されているプリプロセッサーマクロ、CHAR_BITで知ることができる。

C++17では、1バイトはUTF-8の8ビットの1コード単位をすべて表現できると規定している。

std::byte型は、生のバイト列を表すための型として使うことができる。生の1バイトを表すにはunsigned char型が慣習的に使われてきたが、std::byte型は生の1バイトを表現する型として、新たにC++17で追加された。複数バイトが連続するストレージは、unsigned charの配列型、もしくはstd::byteの配列型として表現できる。

std::byte型は、<cstddef>で以下のように定義されている。

namespace std
{
    enum class byte : unsigned char { } ;
}

std::byteはライブラリとしてscoped enum型で定義されている。これにより、他の整数型からの暗黙の型変換が行えない。

0x12std::byte型の変数は以下のように定義できる。

int main()
{
    std::byte b{0x12} ;
}

std::byte型の値がほしい場合は、以下のように書くことができる。

int main()
{
    std::byte b{} ;

    b = std::byte( 1 ) ;
    b = std::byte{ 1 } ;
    b = static_cast< std::byte >( 1 ) ;
    b = static_cast< std::byte >( 0b11110000 ) ;
}

std::byte型は他の数値型からは暗黙に型変換できない。これによりうっかりと型を取り違えてバイト型と他の型を演算してしまうことを防ぐことができる。

int main()
{
    // エラー、()による初期化はint型からの暗黙の変換が入る
    std::byte b1(1) ;

    // エラー、=による初期化はint型からの暗黙の変換が入る
    std::byte b2 = 1 ;

    std::byte b{} ;

    // エラー、operator =によるint型の代入は暗黙の変換が入る
    b = 1 ;
    // エラー、operator =によるdouble型の代入は暗黙の変換が入る
    b = 1.0 ;
}

std::byte型は{}によって初期化するが、縮小変換を禁止するルールにより、std::byte型が表現できる値の範囲でなければエラーとなる。

たとえば、今std::byteが8ビットで、最小値が0、最大値が255の環境だとする。

int main()
{
    // エラー、表現できる値の範囲ではない
    std::byte b1{-1} ;
    // エラー、表現できる値の範囲ではない
    std::byte b2{256} ;
}

std::byteは内部のストレージをバイト単位でアクセスできるようにするため、規格上charと同じような配慮が行われている。

int main()
{
    int x = 42 ;

    std::byte * rep = reinterpret_cast< std::byte * >(&x) ;
}

std::byteは一部の演算子がオーバーロードされているので、通常の整数型のように使うことができる。ただし、バイトをビット列演算するのに使う一部の演算子だけだ。

具体的には、以下に示すシフト、ビットOR, ビット列AND, ビット列XOR, ビット列NOTだ。

<<= << 
>>= >>
|=  |
&=  &
^=  ^
~

四則演算などの演算子はサポートしていない。

std::byteはstd::to_integer<IntType>(std::byte)により、IntType型の整数型に変換できる。

int main()
{
    std::byte b{42} ;

    // int型の値は42
    auto i = std::to_integer<int>(b) ;
}

C++17の型安全な値を格納するライブラリ

C++17では型安全に値を格納するライブラリとして、variant, any, optionalが追加された。

variant : 型安全なunion

使い方

ヘッダーファイル<variant>で定義されているvariantは、型安全なunionとして使うことができる。

#include <variant>

int main()
{
    using namespace std::literals ;

    // int, double, std::stringのいずれかを格納するvariant
    // コンストラクターは最初の型をデフォルト構築
    std::variant< int, double, std::string > x ;

    x = 0 ;         // intを代入
    x = 0.0 ;       // doubleを代入
    x = "hello"s ;  // std::stringを代入

    // intが入っているか確認
    // falseを返す
    bool has_int = std::holds_alternative<int>( x ) ;
    // std::stringが入っているか確認
    // trueを返す
    bool has_string = std::holds_alternative<std::string> ( x ) ;

    // 入っている値を得る
    // "hello"
    std::string str = std::get<std::string>(x) ;
}

型非安全な古典的union

C++が従来から持っている古典的なunionは、複数の型のいずれか1つだけの値を格納する型だ。unionのサイズはデータメンバーのいずれかの型を1つ表現できるだけのサイズとなる。

union U
{
    int i ;
    double d ;
    std::string s ;
} ;

struct S
{
    int i ;
    double d ;
    std::string s ;
}

この場合、sizeof(U)

\[\texttt{sizeof(U)} = \max \{ \texttt{sizeof(int)}, \texttt{sizeof(double)}, \texttt{sizeof(std::string)} \} + \texttt{パディングなど}\]

になる。sizeof(S)は、

\[\texttt{sizeof(S)} = \texttt{sizeof(int)} + \texttt{sizeof(double)} + \texttt{sizeof(std::string)} + \texttt{パディングなど}\]

になる。

unionはメモリー効率がよい。unionvariantと違い型非安全だ。どの型の値を保持しているかという情報は保持しないので、ユーザーが適切に管理しなければならない。

試しに、冒頭のコードをunionで書くと、以下のようになる。

union U
{
    int i ;
    double d ;
    std::string s ;

    // コンストラクター
    // int型をデフォルト初期化する
    U() : i{} { }
    // デストラクター
    // 何もしない。オブジェクトの破棄はユーザーの責任に任せる
    ~U() { }
} ;

// デストラクター呼び出し
template < typename T >
void destruct ( T & x )
{
    x.~T() ;
}

int main()
{
    U u ;

    // 基本型はそのまま代入できる
    // 破棄も考えなくてよい
    u.i = 0 ;
    u.d = 0.0 ;

    // 非トリビアルなコンストラクターを持つ型
    // placement newが必要
    new(&u.s) std::string("hello") ;

    // ユーザーはどの型を入れたか別に管理しておく必要がある
    bool has_int = false ;
    bool has_string = true ;

    std::cout << u.s << '\n' ;

    // 非トリビアルなデストラクターを持つ型
    // 破棄が必要
    destruct( u.s ) ;
}

このようなコードは書きたくない。variantを使えば、このような面倒で冗長なコードを書かずに、型安全にunionと同等機能を実現できる。

variantの宣言

variantはテンプレート実引数で保持したい型を与える。

std::variant< char, short, int, long > v1 ;
std::variant< int, double, std::string > v2 ;
std::variant< std::vector<int>, std::list<int> > v3 ;

variantの初期化

デフォルト初期化

variantはデフォルト構築すると、最初に与えた型の値をデフォルト構築して保持する。

// int
std::variant< int, double > v1 ;
// double
std::variant< double, int > v2 ;

variantにデフォルト構築できない型を最初に与えると、variantもデフォルト構築できない。

// デフォルト構築できない型
struct non_default_constructible
{
    non_default_constructible() = delete ;
} ;

// エラー
// デフォルト構築できない
std::variant< non_default_constructible > v ;

デフォルト構築できない型だけを保持するvariantをデフォルト構築するためには、最初の型をデフォルト構築可能な型にすればよい。

struct A { A() = delete ; } ;
struct B { B() = delete ; } ;
struct C { C() = delete ; } ;

struct Empty { } ;


int main()
{
    // OK、Emptyを保持
    std::variant< Empty, A, B, C > v ;
}

このような場合に、Emptyのようなクラスをわざわざ独自に定義するのは面倒なので、標準ライブラリにはstd::monostateクラスが以下のように定義されている。

namespace std {
    struct monostate { } ;
}

したがって、上の例は以下のように書ける。

// OK、std::monostateを保持
std::variant< std::monostate, A, B, C > v ;

std::monostatevariantの最初のテンプレート実引数として使うことでvariantをデフォルト構築可能にするための型だ。それ以上の意味はない。

コピー初期化

variantに同じ型のvariantを渡すと、コピー/ムーブする。

int main()
{
    std::variant<int> a ;
    // コピー
    std::variant<int> b ( a ) ;
}

variantのコンストラクターに値を渡した場合

variantのコンストラクターに上記以外の値を渡した場合、variantのテンプレート実引数に指定した型の中から、オーバーロード解決により最適な型が選ばれ、その型の値に変換され、値を保持する。

using val = std::variant< int, double, std::string > ;

int main()
{
    // int
    val a(42) ;
    // double
    val b( 0.0 ) ; 

    // std::string
    // char const *型はstd::string型に変換される
    val c("hello") ;

    // int
    // char型はIntegral promotionによりint型に優先的に変換される
    val d( 'a' ) ;
}

in_place_typeによるemplace構築

variantのコンストラクターの第一引数にstd::in_place_type<T>を渡すことにより、T型の要素を構築するためにT型のコンストラクターに渡す実引数を指定できる。

ほとんどの型はコピーかムーブができる。

struct X
{
    X( int, int, int ) { }
} ;

int main()
{
    // Xを構築
    X x( a, b, c ) ;
    // xをコピー
    std::variant<X> v( x ) ;
}

しかし、もし型Xがコピーもムーブもできない型だったとしたら、上記のコードは動かない。

struct X
{
    X( int, int, int ) { }
    X( X const & ) = delete ;
    X( X && ) = delete ; 
} ;

int main()
{
    // Xを構築
    X x( 1, 2, 3 ) ;
    // エラー、Xはコピーできない
    std::variant<X> v( x ) ;
}

このような場合、variantが内部でXを構築する際に、構築に必要なコンストラクターの実引数を渡して、variantXを構築させる必要がある。そのためにstd::in_place_type<T>が使える。Tに構築したい型を指定して第一引数とし、第二引数以降をTのコンストラクターに渡す値にする。

struct X
{
    X( int, int, int ) { }
    X( X const & ) = delete ;
    X( X && ) = delete ; 
} ;

int main()
{
    // Xの値を構築して保持
    std::variant<X> v( std::in_place_type<X>, 1, 2, 3 ) ;
}

variantの破棄

variantのデストラクターは、そのときに保持している値を適切に破棄してくれる。

int main()
{
    std::vector<int> v ;
    std::list<int> l ;
    std::deque<int> d ;
    std::variant< 
        std::vector<int>, 
        std::list<int>,
        std::deque<int>
    > val ;

    val = v ;
    val = l ;
    val = d ;

    // variantのデストラクターはdeque<int>を破棄する
}

variantのユーザーは何もする必要がない。

variantの代入

variantの代入はとても自然だ。variantを渡せばコピーするし、値を渡せばオーバーロード解決に従って適切な型の値を保持する。

variantのemplace

variantemplaceをサポートしている。variantの場合、構築すべき型を知らせる必要があるので、emplace<T>Tで構築すべき型を指定する。

struct X
{
    X( int, int, int ) { }
    X( X const & ) = delete ;
    X( X && ) = delete ; 
} ;

int main()
{
    std::variant<std::monostate, X, std::string> v ;

    // Xを構築
    v.emplace<X>( 1, 2, 3 ) ;
    // std::stringを構築
    v.emplace< std::string >( "hello" ) ;
}

variantに値が入っているかどうかの確認

valueless_by_exceptionメンバー関数

constexpr bool valueless_by_exception() const noexcept;

valueless_by_exceptionメンバー関数は、variantが値を保持している場合、falseを返す。

void f( std::variant<int> & v )
{

    if ( v.valueless_by_exception() ) 
    { // true
        // vは値を保持していない
    }
    else
    { // false
        // vは値を保持している
    }
}

variantはどの値も保持しない状態になることがある。たとえば、std::stringはコピーにあたって動的なメモリー確保を行うかもしれない。variantstd::stringをコピーする際に、動的メモリー確保に失敗した場合、コピーは失敗する。なぜならば、variantは別の型の値を構築する前に、以前の値を破棄しなければならないからだ。variantは値を持たない状態になりうる。

int main()
{
    std::variant< int, std::string > v ;
    try {
        std::string s("hello") ;
        v = s ; // 動的メモリー確保が発生するかもしれない
    } catch( std::bad_alloc e )
    {
        // 動的メモリー確保が失敗するかもしれない
    }

    // 動的メモリー確保の失敗により
    // trueになるかもしれない
    bool b = v.valueless_by_exception() ;
}

indexメンバー関数

constexpr size_t index() const noexcept;

indexメンバー関数は、variantに指定したテンプレート実引数のうち、現在variantが保持している値の型を0ベースのインデックスで返す。

int main()
{
    std::variant< int, double, std::string > v ;

    auto v0 = v.index() ; // 0
    v = 0.0 ;
    auto v1 = v.index() ; // 1
    v = "hello" ;
    auto v2 = v.index() ; // 2
}

もしvariantが値を保持しない場合、つまりvalueless_by_exception()trueを返す場合は、std::variant_nposを返す。

// variantが値を持っているかどうか確認する関数
template < typename ... Types  >
void has_value( std::variant< Types ... > && v )
{
    return v.index() != std::variant_npos ;

    // これでもいい
    // return v.valueless_by_exception() == false ;
}

std::variant_nposの値は\(-1\)だ。

swap

variantswapに対応している。

int main()
{
    std::variant<int> a, b ;

    a.swap(b) ;
    std::swap( a, b ) ;
}

variant_size<T> : variantが保持できる型の数を取得

std::variant_size<T>は、Tvariant型を渡すと、variantが保持できる型の数を返してくれる。

using t1 = std::variant<char> ;
using t2 = std::variant<char, short> ;
using t3 = std::variant<char, short, int> ;

// 1
constexpr std::size_t t1_size = std::variant_size<t1>::size ;
// 2
constexpr std::size_t t2_size = std::variant_size<t2>::size ;
// 3
constexpr std::size_t t2_size = std::variant_size<t3>::size ;

variant_sizeはintegral_constantを基本クラスに持つクラスなので、デフォルト構築した結果をユーザー定義変換することでも値を取り出せる。

using type = std::variant<char, short, int> ;

constexpr std::size_t size = std::variant_size<type>{} ;

variant_sizeを以下のようにラップした変数テンプレートも用意されている。

template <class T>
    inline constexpr size_t variant_size_v = variant_size<T>::value;

これを使えば、以下のようにも書ける。

using type = std::variant<char, short, int> ;

constexpr std::size_t size = std::variant_size_v<type> ;

variant_alternative<I, T> : インデックスから型を返す

std::variant_alternative<I, T>T型のvariantの保持できる型のうち、I番目の型をネストされた型名typeで返す。

using type = std::variant< char, short, int > ;

// char
using t0 = std::variant_alternative< 0, type >::type ;
// short
using t1 = std::variant_alternative< 1, type >::type ;
// int
using t2 = std::variant_alternative< 2, type >::type ;

variant_alternative_tというテンプレートエイリアスが以下のように定義されている。

template <size_t I, class T>
    using variant_alternative_t 
        = typename variant_alternative<I, T>::type ;

これを使えば、以下のようにも書ける。

using type = std::variant< char, short, int > ;

// char
using t0 = std::variant_alternative_t< 0, type > ;
// short
using t1 = std::variant_alternative_t< 1, type > ;
// int
using t2 = std::variant_alternative_t< 2, type > ;

holds_alternative : variantが指定した型の値を保持しているかどうかの確認

holds_alternative<T>(v)は、variant vT型の値を保持しているかどうかを確認する。保持しているのであればtrueを、そうでなければfalseを返す。

int main()
{
    // int型の値を構築
    std::variant< int, double > v ;

    // true
    bool has_int = std::holds_alternative<int>(v) ;
    // false
    bool has_double = std::holds_alternative<double>(v) ;
}

Tは実引数に与えられたvariantが保持できる型でなければならない。以下のようなコードはエラーとなる。

int main()
{
    std::variant< int > v ;

    // エラー
    std::holds_alternative<double>(v) ;
}

get<I>(v) : インデックスから値の取得

get<I>(v)は、variant vの型のインデックスからI番目の型の値を返す。インデックスは0ベースだ。

int main()
{
    // 0: int
    // 1: double
    // 2: std::string
    std::variant< int, double, std::string > v(42) ;

    // int, 42
    auto a = std::get<0>(v) ;

    v = 3.14 ;
    // double, 3.14
    auto b = std::get<1>(v) ;

    v = "hello" ;
    // std::string, "hello"
    auto c = std::get<2>(v) ;
}

Iがインデックスの範囲を超えているとエラーとなる。

int main()
{
    // インデックスは0, 1, 2まで
    std::variant< int, double, std::string > v ;

    // エラー、範囲外
    std::get<3>(v) ;
}

もし、variantが値を保持していない場合、つまりv.index() != Iの場合は、std::bad_variant_accessthrowされる。

int main()
{
    // int型の値を保持
    std::variant< int, double > v( 42 ) ;

    try {
        // double型の値を要求
        auto d = std::get<1>(v) ;
    } catch ( std::bad_variant_access & e )
    {
        // doubleは保持していなかった
    }
}

getの実引数に渡すvariantlvalueの場合は、戻り値はlvalueリファレンス、rvalueの場合は戻り値はrvalueリファレンスになる。

int main()
{
    std::variant< int > v ;

    // int &
    decltype(auto) a = std::get<0>(v) ;
    // int &&
    decltype(auto) b = std::get<0>( std::move(v) ) ;
}

getの実引数に渡すvariantがCV修飾されている場合、戻り値の型も実引数と同じくCV修飾される。

int main()
{
    std::variant< int > const cv ;
    std::variant< int > volatile vv ;
    std::variant< int > const volatile cvv ;

    // int const &
    decltype(auto) a = std::get<0>( cv ) ;
    // int volatile &
    decltype(auto) b = std::get<0>( vv ) ;
    // int const volatile &
    decltype(auto) c = std::get<0>( cvv ) ;
}

get<T>(v) : 型から値の取得

get<T>(v)は、variant vの保有する型Tの値を返す。型Tの値を保持していない場合、std::bad_variant_accessthrowされる。

int main()
{
    std::variant< int, double, std::string > v( 42 ) ;

    // int
    auto a = std::get<int>( v ) ;

    v = 3.14 ;
    // double
    auto b = std::get<double>( v ) ;

    v = "hello" ;
    // std::string
    auto c = std::get<std::string>( v ) ;
}

その他はすべてget<I>と同じ。

get_if : 値を保持している場合に取得

get_if<I>(vp)get_if<T>(vp)は、variantへのポインターvpを実引数に取り、*vpがインデックスI, もしくは型Tの値を保持している場合、その値へのポインターを返す。

int main()
{
    std::variant< int, double, std::string > v( 42 ) ;

    // int *
    auto a = std::get_if<int>( &v ) ; 

    v = 3.14 ;
    // double *
    auto b = std::get_if<1>( &v ) ;

    v = "hello" ;
    // std::string
    auto c = std::get_if<2>( &v ) ;

}

もし、vpnullptrの場合、もしくは*vpが指定された値を保持していない場合は、nullptrを返す。

int main()
{
    // int型の値を保持
    std::variant< int, double > v( 42 ) ;

    // nullptr
    auto a = std::get_if<int>( nullptr ) ;

    // nullptr
    auto a = std::get_if<double>( &v ) ;
}

variantの比較

variantは比較演算子がオーバーロードされているため比較できる。variant同士の比較は、一般のプログラマーは自然だと思う結果になるように実装されている。

同一性の比較

variantの同一性の比較のためには、variantのテンプレート実引数に与える型は自分自身と比較可能でなければならない。

つまり、variant v, wに対して、式get<i>(v) == get<i>(w)がすべてのiに対して妥当でなければならない。

variant v, wの同一性の比較は、v == wの場合、以下のように行われる。

  1. v.index() != w.index()ならば、false
  2. それ以外の場合、v.value_less_by_exception()ならば、true
  3. それ以外の場合、get<i>(v) == get<i>(w)。ただしiv.index()

2つのvariantが別の型を保持している場合は等しくない。ともに値なしの状態であれば等しい。それ以外は保持している値同士が比較される。

int main()
{
    std::variant< int, double > a(0), b(0) ;

    // true
    // 同じ型の同じ値を保持している
    a == b ;

    a = 1.0 ;

    // false
    // 型が違う
    a == b ;
}

たとえばoperator ==は以下のような実装になる。

template <class... Types>
constexpr bool 
operator == (const variant<Types...>& v, const variant<Types...>& w)
{
    if ( v.index() != w.index() )
        return false ;
    else if ( v.valueless_by_exception() )
        return true ;
    else
        return std::visit( 
            []( auto && a, auto && b ){ return a == b ; },
            v, w ) ;
}

operator !=はこの逆だと考えてよい。

大小比較

variantの大小の比較のためには、variantのテンプレート実引数に与える型は自分自身と比較可能でなければならない。

つまり、operator <の場合、variant v, wに対して、式get<i>(v) < get<i>(w)がすべてのiに対して妥当でなければならない。

variant v, wの大小比較は、v < wの場合、以下のように行われる。

  1. w.valueless_by_exception()ならば、false
  2. それ以外の場合、v.valueless_by_exception()ならば、true
  3. それ以外の場合、v.index() < w.index()ならば、true
  4. それ以外の場合、v.index() > w.index()ならば、false
  5. それ以外の場合、get<i>(v) < get<i>(w)。ただしiv.index()

値なしのvariantは最も小さいとみなされる。インデックスの小さいほうが小さいとみなされる。どちらも同じ型の値があるのであれば、値同士の比較となる。

int main()
{
    std::variant< int, double > a(0), b(0) ;

    // false
    // 同じ型の同じ値を比較
    a < b ;

    a = 1.0 ;

    // false
    // インデックスによる比較
    a < b ;
    // true
    // インデックスによる比較
    b < a ;
}

operator <は以下のような実装になる。

template <class... Types>
constexpr bool 
operator<(const variant<Types...>& v, const variant<Types...>& w)
{
    if ( w.valueless_by_exception() )
        return false ;
    else if ( v.valueless_by_exception() )
        return true ;
    else if ( v.index() < w.index() )
        return true ;
    else if ( v.index() > w.index() )
        return false ;
    else
        return std::visit( 
            []( auto && a, auto && b ){ return a < b ; },
            v, w ) ;
}

残りの大小比較も同じ方法で比較される。

visit : variantが保持している値を受け取る

std::visitは、variantの保持している型を実引数に関数オブジェクトを呼んでくれるライブラリだ。

int main()
{
    using val = std::variant<int, double> ;

    val v(42) ;
    val w(3.14) ;

    auto visitor =  []( auto a, auto b ) 
                    { std::cout << a << b << '\n' ; } ;

    // visitor( 42, 3.14 )が呼ばれる
    std::visit( visitor, v, w ) ;
    // visitor( 3.14, 42 ) が呼ばれる
    std::visit( visitor, w, v ) ;
}

このように、variantにどの型の値が保持されていても扱うことができる。

std::visitは以下のように宣言されている。

template < class Visitor, class... Variants >
constexpr auto visit( Visitor&& vis, Variants&&... vars ) ;

第一引数に関数オブジェクトを渡し、第二引数以降にvariantを渡す。すると、vis( get<i>(vars)... )のように呼ばれる。

int main()
{
    std::variant<int> a(1), b(2), c(3) ;

    // ( 1 ) 
    std::visit( []( auto x ) {}, a ) ;

    // ( 1, 2, 3 )
    std::visit( []( auto x, auto y, auto z ) {}, a, b, c ) ;
}

any : どんな型の値でも保持できるクラス

使い方

ヘッダーファイル<any>で定義されているstd::anyは、ほとんどどんな型の値でも保持できるクラスだ。

#include <any>

int main()
{
    std::any a ;

    a = 0 ; // int
    a = 1.0 ; // double
    a = "hello" ; // char const *

    std::vector<int> v ;
    a = v ; // std::vector<int>

    // 保持しているstd::vector<int>のコピー
    auto value = std::any_cast< std::vector<int> >( a ) ;
}

anyが保持できない型は、コピー構築できない型だ。

anyの構築と破棄

クラスanyはテンプレートではない。そのため宣言は単純だ。

int main()
{
    // 値を保持しない
    std::any a ;
    // int型の値を保持する
    std::any b( 0 ) ;
    // double型の値を保持する
    std::any c( 0.0 ) ;
}

anyが保持する型を事前に指定する必要はない。

クラスanyを破棄すると、そのとき保持していた値が適切に破棄される。

in_place_typeコンストラクター

anyのコンストラクターでemplaceをするためにin_place_typeが使える。

struct X
{
    X( int, int ) { }
} ;

int main()
{
    // 型XをX(1, 2)で構築した結果の値を保持する
    std::any a( std::in_place_type<X>, 1, 2 ) ;
}

anyへの代入

anyへの代入も普通のプログラマーの期待どおりの動きをする。

int main()
{
    std::any a ;
    std::any b ;

    // aはint型の値42を保持する
    a = 42 ;
    // bはint型の値42を保持する
    b = a ;
    
}

anyのメンバー関数

emplace

template <class T, class... Args>
decay_t<T>& emplace(Args&&... args);

anyemplaceメンバー関数をサポートしている。

struct X
{
    X( int, int ) { }
} ;

int main()
{
    std::any a ;

    // 型XをX(1, 2)で構築した結果の値を保持する
    a.emplace<X>( 1, 2 ) ;
}

reset : 値の破棄

void reset() noexcept ; 

anyresetメンバー関数は、anyの保持してある値を破棄する。resetを呼び出した後のanyは値を保持しない。

int main()
{
    // aは値を保持しない
    std::any a ;
    // aはint型の値を保持する
    a = 0 ;

    // aは値を保持しない
    a.reset() ;
}

swap : スワップ

anyswapメンバー関数をサポートしている。

int main()
{
    std::any a(0) ;
    std::any b(0.0) ;

    // aはint型の値を保持
    // bはdouble型の値を保持

    a.swap(b) ;

    // aはdouble型の値を保持
    // bはint型の値を保持
}

has_value : 値を保持しているかどうか調べる

bool has_value() const noexcept;

anyhas_valueメンバー関数はanyが値を保持しているかどうかを調べる。値を保持しているならばtrueを、保持していないならばfalseを返す。

int main()
{
    std::any a ;

    // false
    bool b1 = a.has_value() ;

    a = 0 ;
    // true
    bool b2 = a.has_value() ;

    a.reset() ;
    // false
    bool b3 = a.has_value() ;
}

type : 保持している型のtype_infoを得る

const type_info& type() const noexcept;

typeメンバー関数は、保持している型Ttypeid(T)を返す。値を保持していない場合、typeid(void)を返す。

int main()
{
    std::any a ;

    // typeid(void)
    auto & t1 = a.type() ;

    a = 0 ;
    // typeid(int)
    auto & t2 = a.type() ;

    a = 0.0 ;
    // typeid(double)
    auto & t3 = a.type() ;
}

anyのフリー関数

make_any<T> : T型のanyを作る

template <class T, class... Args>
any make_any(Args&& ...args);

template <class T, class U, class... Args>
any make_any(initializer_list<U> il, Args&& ...args);

make_any<T>( args... )T型をコンストラクター実引数args...で構築した値を保持するanyを返す。

struct X
{
    X( int, int ) { }
} ;

int main()
{
    // int型の値を保持するany
    auto a = std::make_any<int>( 0 ) ;
    // double型の値を保持するany
    auto b = std::make_any<double>( 0.0 ) ;

    // X型の値を保持するany
    auto c = std::make_any<X>( 1, 2 ) ;
}

any_cast : 保持している値の取り出し

template<class T> T any_cast(const any& operand);
template<class T> T any_cast(any& operand);
template<class T> T any_cast(any&& operand);

any_cast<T>(operand)operandが保持している値を返す。

int main()
{
    std::any a(0) ;

    int value = std::any_cast<int>(a) ;
}

any_cast<T>で指定したT型が、anyが保持している型ではない場合、std::bad_any_castthrowされる。

int main()
{
    try {
        std::any a ;
        std::any_cast<int>(a) ;
    } catch( std::bad_any_cast e )
    {
        // 型を保持していなかった
    }

}
template<class T>
const T* any_cast(const any* operand) noexcept;
template<class T>
T* any_cast(any* operand) noexcept;

any_cast<T>anyへのポインターを渡すと、Tへのポインター型が返される。anyT型を保持している場合はT型を参照するポインターが返る。保持していない場合は、nullptrが返る。

int main()
{
    std::any a(42) ;

    // int型の値を参照するポインター
    int * p1 = std::any_cast<int>( &a ) ;

    // nullptr
    double * p2 = std::any_cast<double>( &a ) ;
}

optional : 値を保有しているか、していないクラス

使い方

ヘッダーファイル<optional>で定義されているoptional<T>は、T型の値を保有しているか、保有していないライブラリだ。

条件次第で値が用意できない場合が存在する。たとえば割り算の結果の値を返す関数を考える。

int divide( int a, int b )
{
    if ( b == 0 )
    {
        // エラー処理
    }
    else
        return a / b ;
}

ゼロで除算はできないので、bの値が0の場合、この関数は値を用意することができない。問題は、int型のすべての値は通常の除算結果として使われるので、エラーであることを示す特別な値を返すこともできない。

このような場合にエラーや値を通知する方法として、過去にさまざまな方法が考案された。たとえば、ポインターやリファレンスを実引数として受け取る方法、グローバル変数を使う方法、例外だ。

optionalはこのような値が用意できない場合に使える共通の方法を提供する。

std::optional<int> divide( int a, int b )
{
    if ( b == 0 )
        return {} ;
    else
        return { a / b } ;
}

int main()
{
    auto result = divide( 10, 2 ) ;
    // 値の取得
    auto value = result.value() ;

    // ゼロ除算
    auto fail = divide( 10, 0 ) ;

    // false、値を保持していない
    bool has_value = fail.has_value() ;

    // throw bad_optional_access
    auto get_value_anyway = fail.value() ;
}

optionalのテンプレート実引数

optional<T>T型の値を保持するか、もしくは保持しない状態を取る。

int main()
{
    // int型の値を保持するかしないoptional
    using a = std::optional<int> ;
    // double型の値を保持するかしないoptional
    using b = std::optional<double> ;
}

optionalの構築

optionalをデフォルト構築すると、値を保持しないoptionalになる。

int main()
{
    // 値を保持しない
    std::optional<int> a ;
}

コンストラクターの実引数にstd::nulloptを渡すと、値を保持しないoptionalになる。

int main()
{
    // 値を保持しない
    std::optional<int> a( std::nullopt ) ;
}

optional<T>のコンストラクターの実引数にT型に変換できる型を渡すと、T型の値に型変換して保持する。

int main()
{
    // int型の値42を保持する
    std::optional<int> a(42) ;

    // double型の値1.0を保持する
    std::optional<double> b( 1.0 ) ;

    // intからdoubleへの型変換が行われる
    // int型の値1を保持する
    std::optional<int> c ( 1.0 ) ;
}

T型からU型に型変換できるとき、optional<T>のコンストラクターにoptional<U>を渡すとUからTに型変換されてT型の値を保持するoptionalになる。

int main()
{
    // int型の値42を保持する
    std::optional<int> a( 42 ) ;

    // long型の値42を保持する
    std::optional<long> b ( a ) ;
}

optionalのコンストラクターの第一引数にstd::in_place_type<T>を渡すと、後続の引数を使ってT型のオブジェクトがemplace構築される。

struct X
{
    X( int, int ) { }
} ;

int main()
{
    // X(1, 2)
    std::optional<X> o( std::in_place_type<X>, 1, 2 ) ;
}

optionalの代入

通常のプログラマーの期待どおりの挙動をする。std::nulloptを代入すると値を保持しないoptionalになる。

optionalの破棄

optionalが破棄されるとき、保持している値があれば、適切に破棄される。

struct X
{
    ~X() { }
} ;

int main()
{
    {
        // 値を保持する
        std::optional<X> o ( X{} ) ;
        // Xのデストラクターが呼ばれる
    }

    {
        // 値を保持しない
        std::optional<X> o ;
        // Xのデストラクターは呼ばれない
    }   
}

swap

optionalswapに対応している。

int main()
{
    std::optional<int> a(1), b(2) ;

    a.swap(b) ;
}

has_value : 値を保持しているかどうか確認する

constexpr bool has_value() const noexcept;

has_valueメンバー関数はoptionalが値を保持している場合、trueを返す。

int main()
{
    std::optional<int> a ;
    // false
    bool b1 = a.has_value() ;

    std::optional<int> b(42) ;
    // true
    bool b2 = b.has_value() ;
}

operator bool : 値を保持しているかどうか確認する

constexpr explicit operator bool() const noexcept;

optionalを文脈上boolに変換すると、値を保持している場合にのみtrueとして評価される。

int main()
{
    std::optional<bool> a = some_function();
    // OK、文脈上boolに変換
    if ( a )
    {
        // 値を保持
    }
    else
    {
        // 値を不保持
    }

    // エラー、暗黙の型変換は行われない
    bool b1 = a ;
    // OK、明示的な型変換
    bool b2 = static_cast<bool>(a) ;
}

value : 保持している値を取得

constexpr const T& value() const&;
constexpr T& value() &;
constexpr T&& value() &&;
constexpr const T&& value() const&&;

valueメンバー関数はoptionalが値を保持している場合、値へのリファレンスを返す。値を保持していない場合、std::bad_optional_accessthrowされる。

int main()
{
    std::optional<int> a(42) ;

    // OK
    int x = a.value () ;

    try {
        std::optional<int> b ;
        int y = b.value() ;
    } catch( std::bad_optional_access e )
    {
        // 値を保持していなかった
    }
}

value_or : 値もしくはデフォルト値を返す

template <class U> constexpr T value_or(U&& v) const&;
template <class U> constexpr T value_or(U&& v) &&;

value_or(v)メンバー関数は、optionalが値を保持している場合はその値を、保持していない場合はvを返す。

int main()
{
    std::optional<int> a( 42 ) ;

    // 42
    int x = a.value_or(0) ;

    std::optional<int> b ;

    // 0
    int x = b.value_or(0) ;
}

reset : 保持している値を破棄する

resetメンバー関数を呼び出すと、保持している値がある場合破棄する。resetメンバー関数を呼び出した後のoptionalは値を保持しない状態になる。

int main()
{
    std::optional<int> a( 42 ) ;

    // true
    bool b1 = a.has_value() ;

    a.reset() ;

    // false
    bool b2 = a.has_value() ;
}

optional同士の比較

optional<T>を比較するためには、T型のオブジェクト同士が比較できる必要がある。

同一性の比較

値を保持しない2つのoptionalは等しい。片方のみが値を保持しているoptionalは等しくない。両方とも値を保持しているoptionalは値による比較になる。

int main()
{
    std::optional<int> a, b ;

    // true
    // どちらも値を保持しないoptional
    bool b1 = a == b ;

    a = 0 ;

    // false
    // aのみ値を保持
    bool b2 = a == b ;

    b = 1 ;

    // false
    // どちらも値を保持。値による比較
    bool b3 = a == b ;
}

大小比較

optional同士の大小比較は、a < bの場合

  1. bが値を保持していなければfalse
  2. それ以外の場合で、aが値を保持していなければtrue
  3. それ以外の場合、abの保持している値同士の比較

となる。

int main()
{
    std::optional<int> a, b ;

    // false
    // bが値なし
    bool b1 = a < b ;

    b = 0 ;

    // true
    // bは値ありでaが値なし
    bool b2 = a < b ;

    a = 1 ;

    // false
    // どちらとも値があるので値同士の比較
    // 1 < 0はfalse
    bool b3 = a < b ;
}

optionalとstd::nulloptとの比較

optionalstd::nulloptとの比較は、std::nulloptが値を持っていないoptionalとして扱われる。

optional<T>とTの比較

optional<T>T型の比較では、optional<t>が値を保持していない場合falseが返る。それ以外の場合、optionalの保持している値とTが比較される。

int main()
{
    std::optional<int> o(1) ;

    // true
    bool b1 = ( o == 1 ) ;
    // false
    bool b2 = ( o == 0 ) ;

    // oは値を保持しない
    o.reset() ;

    // Tの値にかかわらずfalse
    // false
    bool b3 = ( o == 1 ) ;
    // false
    bool b4 = ( o == 0 ) ;
}

make_optional<T> : optional<T>を返す

template <class T>
constexpr optional<decay_t<T>> make_optional(T&& v);

make_optional<T>(T t)optional<T>(t)を返す。

int main()
{
    // std::optional<int>、値は0
    auto o1 = std::make_optional( 0 ) ;

    // std::optional<double>、値は0.0
    auto o2 = std::make_optional( 0.0 ) ;
}

make_optional<T, Args ... > : optional<T>をin_place_type構築して返す

make_optionalの第一引数がT型ではない場合、in_place_type構築するオーバーロード関数が選ばれる。

struct X
{
    X( int, int ) { }
} ;

int main()
{
    // std::optional<X>( std::in_place_type<X>, 1, 2 )
    auto o = std::make_optional<X>( 1, 2 ) ;
}

string_view : 文字列ラッパー

string_viewは、文字型(char, wchar_t, char16_t, char32_t)の連続した配列で表現された文字列に対する共通の文字列ビューを提供する。文字列は所有しない。

使い方

連続した文字型の配列を使った文字列の表現方法にはさまざまある。C++では最も基本的な文字列の表現方法として、null終端された文字型の配列がある。

char str[6] = { 'h', 'e', 'l', 'l', 'o', '\0' } ;

あるいは、文字型の配列と文字数で表現することもある。

// sizeは文字数
std::size_t size
char * ptr ;

このような表現をいちいち管理するのは面倒なので、クラスで包むこともある。

class string_type
{
    std::size_t size ;
    char *ptr
} ;

このように文字列を表現する方法はさまざまある。これらのすべてに対応していると、表現の数だけ関数のオーバーロードが追加されていくことになる。

// null終端文字列用
void process_string( char * ptr ) ;
// 配列へのポインターと文字数
void process_string( char * ptr, std::size_t size ) ;
// std::stringクラス
void process_string( std::string s ) ;
// 自作のstring_typeクラス
void process_string( string_type s ) ;
// 自作のmy_string_typeクラス
void process_string( my_string_type s ) ;

string_viewはさまざまな表現の文字列に対して共通のviewを提供することで、この問題を解決できる。もう関数のオーバーロードを大量に追加する必要はない。

// 自作のstring_type
struct string_type
{
    std::size_t size ;
    char * ptr ;

    // string_viewに対応する変換関数
    operator std::string_view() const noexcept
    {
        return std::string_view( ptr, size ) ;
    }
}

// これ1つだけでよい
void process_string( std::string_view s ) ;

int main()
{
    // OK
    process_string( "hello" ) ;
    // OK
    process_string( { "hello", 5 } ) ;

    std::string str( "hello" ) ;
    process_string( str ) ;

    string_type st{5, "hello"} ;

    process_string( st ) ;
}

basic_string_view

std::stringがstd::basic_string< CharT, Traits >に対するstd::basic_string<char>であるように、std::string_viewも、その実態はstd::basic_string_viewの特殊化へのtypedef名だ。

// 本体
template<class charT, class traits = char_traits<charT>>
class basic_string_view ;

// それぞれの文字型のtypedef名
using string_view = basic_string_view<char>;
using u16string_view = basic_string_view<char16_t>;
using u32string_view = basic_string_view<char32_t>;
using wstring_view = basic_string_view<wchar_t>;

なので、通常はbasic_string_viewではなく、string_viewとかu16string_viewなどのtypedef名を使うことになる。本書ではstring_viewだけを解説するが、その他のtypedef名も文字型が違うだけで同じだ。

文字列の所有、非所有

string_viewは文字列を所有しない。所有というのは、文字列を表現するストレージの確保と破棄に責任を持つということだ。所有しないことの意味を説明するために、まず文字列を所有するライブラリについて説明する。

std::stringは文字列を所有する。std::string風のクラスの実装は、たとえば以下のようになる。

class string
{
    std::size_t size ;
    char * ptr ;

public :
    // 文字列を表現するストレージの動的確保
    string ( char const * str )
    {
        size = std::strlen( str ) ;
        ptr = new char[size+1] ;
        std::strcpy( ptr, str ) ;
    }

    // コピー
    // 別のストレージを動的確保
    string ( string const & r )
        : size( r.size ), ptr ( new char[size+1] )
    {
        std::strcpy( ptr, r.ptr ) ;
    }

    // ムーブ
    // 所有権の移動
    string ( string && r )
        : size( r.size ), ptr( r.ptr )
    {
        r.size = 0 ;
        r.ptr = nullptr ;
    }

    // 破棄
    // 動的確保したストレージを解放
    ~string()
    {
        delete[] ptr ;
    }
    
} ;

std::stringは文字列を表現するストレージを動的に確保し、所有する。コピーは別のストレージを確保する。ムーブするときはストレージの所有権を移す。デストラクターは所有しているストレージを破棄する。

std::string_viewは文字列を所有しない。std::string_view風のクラスの実装は、たとえば以下のようになる。

class string_view
{
    std::size_t size ;
    char const * ptr ;

public :

    // 所有しない
    // strの参照先の寿命は呼び出し側が責任を持つ
    string_view( char const * str ) noexcept
        : size( std::strlen(str) ), ptr( str )
    { }

    // コピー
    // メンバーごとのコピーだけでよいのでdefault化するだけでよい
    string_view( string_view const & r ) noexcept = default ;

    // ムーブはコピーと同じ
    // 所有しないので所有権の移動もない

    // 破棄
    // 何も解放するストレージはない
    // デストラクターもトリビアルでよい
} ;

string_viewに渡した連続した文字型の配列へのポインターの寿命は、渡した側が責任を持つ。つまり、以下のようなコードは間違っている。

std::string_view get_string()
{
    char str[] = "hello" ;

    // エラー
    // strの寿命は関数の呼び出し元に戻った時点で尽きている
    return str ;
}

string_viewの構築

string_viewの構築には4種類ある。

デフォルト構築

constexpr basic_string_view() noexcept;

string_viewのデフォルト構築は、空のstring_viewを作る。

int main()
{
    // 空のstring_view
    std::string_view s ;
}

null終端された文字型の配列へのポインター

constexpr basic_string_view(const charT* str);

このstring_viewのコンストラクターは、null終端された文字型へのポインターを受け取る。

int main()
{
    std::string_view s( "hello" ) ;
}

文字型へのポインターと文字数

constexpr basic_string_view(const charT* str, size_type len);

このstring_viewのコンストラクターは、文字型の配列へのポインターと文字数を受け取る。ポインターはnull終端されていなくてもよい。

int main()
{
    char str[] = {'h', 'e', 'l', 'l', 'o'} ;

    std::string_view s( str, 5 ) ;
}

文字列クラスからの変換関数

他の文字列クラスからstring_viewを作るには、変換関数を使う。string_viewのコンストラクターは使わない。

std::stringstring_viewへの変換関数をサポートしている。独自の文字列クラスをstring_viewに対応させるにも変換関数を使う。たとえば以下のように実装する。

class string
{
    std::size_t size ;
    char * ptr ;
public :
    operator std::string_view() const noexcept
    {
        return std::string_view( ptr, size ) ;
    }
} ;

これにより、std::stringからstring_viewへの変換が可能になる。

int main()
{
    std::string s = "hello" ;
    std::string_view sv = s ;
}

コレと同じ方法を使えば、独自の文字列クラスもstring_viewに対応させることができる。

std::stringstring_viewを受け取るコンストラクターを持っているので、string_viewからstringへの変換もできる。

int main()
{
    std::string_view sv = "hello" ;

    // コピーされる
    std::string s = sv ;
}

string_viewの操作

string_viewは既存の標準ライブラリのstringとほぼ同じ操作性を提供している。たとえばイテレーターを取ることができるし、operator []で要素にアクセスできるし、size()で要素数が返るし、find()で検索もできる。

template < typename T >
void f( T  t )
{
    for ( auto c : t )
    {
        std::cout << c ;
    }

    if ( t.size() > 3 )
    {
        auto c = t[3] ;
    }

    auto pos = t.find( "fox" ) ;
}

int main()
{
    std::string s("quick brown fox jumps over the lazy dog.") ;

    f( s ) ;

    std::string_view sv = s ;

    f( sv ) ;
}

string_viewは文字列を所有しないので、文字列を書き換える方法を提供していない。

int main()
{
    std::string s = "hello" ;

    s[0] = 'H' ;
    s += ",world" ;

    std::string_view sv = s ;

    // エラー
    // string_viewは書き換えられない
    sv[0] = 'h' ;
    s += ".\n" ;
}

string_viewは文字列を所有せず、ただ参照しているだけだからだ。

int main()
{
    std::string s = "hello" ;
    std::string_view sv = s ;

    // "hello"
    std::cout << sv ;

    s = "world" ;

    // "world"
    // string_viewは参照しているだけ
    std::cout << sv ;
}

string_viewstringとほぼ互換性のあるメンバーを持っているが、一部の文字列を変更するメンバーは削除されている。

remove_prefix/remove_suffix : 先頭、末尾の要素の削除

string_viewは先頭と末尾からn個の要素を削除するメンバー関数を提供している。

constexpr void remove_prefix(size_type n);
constexpr void remove_suffix(size_type n);

string_viewにとって、先頭と末尾からn個の要素を削除するのは、ポインターをn個ずらすだけなので、これは文字列を所有しないstring_viewでも行える操作だ。

int main()
{
    std::string s = "hello" ;

    std::string_view s1 = s ;

    // "lo"
    s1.remove_prefix(3) ;

    std::string_view s2 = s ;

    // "he"
    s2.remove_suffix(3) ;
}

このメンバー関数は既存のstd::stringにも追加されている。

ユーザー定義リテラル

std::stringstd::string_viewにはユーザー定義リテラルが追加されている。

string operator""s(const char* str, size_t len);
u16string operator""s(const char16_t* str, size_t len);
u32string operator""s(const char32_t* str, size_t len);
wstring operator""s(const wchar_t* str, size_t len);

constexpr string_view
operator""sv(const char* str, size_t len) noexcept;

constexpr u16string_view
operator""sv(const char16_t* str, size_t len) noexcept;

constexpr u32string_view
operator""sv(const char32_t* str, size_t len) noexcept;

constexpr wstring_view
operator""sv(const wchar_t* str, size_t len) noexcept;

以下のように使う。

int main()
{
    using namespace std::literals ;

    // std::string
    auto s = "hello"s ;

    // std::string_view
    auto sv = "hello"sv ;
}

メモリーリソース : 動的ストレージ確保ライブラリ

ヘッダーファイル<memory_resource>で定義されているメモリーリソースは、動的ストレージを確保するためのC++17で追加されたライブラリだ。その特徴は以下のとおり。

メモリーリソース

メモリーリソースはアロケーターに変わる新しいメモリー確保と解放のためのインターフェースとしての抽象クラスだ。コンパイル時に挙動を変える静的ポリモーフィズム設計のアロケーターと違い、メモリーリソースは実行時に挙動を変える動的ポリモーフィズム設計となっている。

void f( memory_resource * mem )
{
    // 10バイトのストレージを確保
    auto ptr = mem->allocate( 10 ) ;
    // 確保したストレージを解放
    mem->deallocate( ptr ) ;
}

クラスstd::pmr::memory_resourceの宣言は以下のとおり。

namespace std::pmr {

class memory_resource {
public:
    virtual ~ memory_resource();
    void* allocate(size_t bytes, size_t alignment = max_align);
    void deallocate(void* p, size_t bytes,
                    size_t alignment = max_align);
    bool is_equal(const memory_resource& other) const noexcept;

private:
    virtual void* do_allocate(size_t bytes, size_t alignment) = 0;
    virtual void do_deallocate( void* p, size_t bytes,
                                size_t alignment) = 0;
    virtual bool do_is_equal(const memory_resource& other)
        const noexcept = 0;
};

}

クラスmemory_resourcestd::pmr名前空間スコープの中にある。

メモリーリソースの使い方

memory_resourceを使うのは簡単だ。memory_resourceのオブジェクトを確保したら、メンバー関数allocate( bytes, alignment )でストレージを確保する。メンバー関数deallocate( p, bytes, alignment )でストレージを解放する。

void f( std::pmr::memory_resource * mem )
{
    // 100バイトのストレージを確保
    void * ptr = mem->allocate( 100 ) ;
    // ストレージを解放
    mem->deallocate( ptr, 100 ) ;
}

2つのmemory_resourceのオブジェクトa, bがあるとき、一方のオブジェクトで確保したストレージをもう一方のオブジェクトで解放できるとき、a.is_equal( b )trueを返す。

void f( std::pmr::memory_resource * a, std::pmr::memory_resource * b )
{
    void * ptr = a->allocate( 1 ) ;

    // aで確保したストレージはbで解放できるか?
    if ( a->is_equal( *b ) )
    {// できる
        b->deallocate( ptr, 1 ) ;
    }
    else
    {// できない
        a->deallocate( ptr, 1 ) ;
    }
}

is_equalを呼び出すoperator ==operator !=も提供されている。

void f( std::pmr::memory_resource * a, std::pmr::memory_resource * b )
{
    bool b1 = ( *a == *b ) ;
    bool b2 = ( *a != *b ) ;
}

メモリーリソースの作り方

独自のメモリーアロケーターをmemory_resourceのインターフェースに合わせて作るには、memory_resourceから派生した上で、do_allocate, do_deallocate, do_is_equalの3つのprivate純粋virtualメンバー関数をオーバーライドする。必要に応じてデストラクターもオーバーライドする。

class memory_resource {
    // 非公開
    static constexpr size_t max_align = alignof(max_align_t);

public:
    virtual ~ memory_resource();

private:
    virtual void* do_allocate(size_t bytes, size_t alignment) = 0;
    virtual void do_deallocate( void* p, size_t bytes,
                                size_t alignment) = 0;
    virtual bool do_is_equal(const memory_resource& other)
        const noexcept = 0;
};

do_allocate(bytes, alignment)は少なくともalignmentバイトでアライメントされたbytesバイトのストレージへのポインターを返す。ストレージが確保できなかった場合は、適切な例外をthrowする。

do_deallocate(p, bytes, alignment)は事前に同じ*thisから呼び出されたallocate( bytes, alignment )で返されたポインターpを解放する。すでに解放されたポインターpを渡してはならない。例外は投げない。

do_is_equal(other)は、*thisとotherが互いに一方で確保したストレージをもう一方で解放できる場合にtrueを返す。

たとえば、malloc/freeを使ったmemory_resourceの実装は以下のとおり。

// malloc/freeを使ったメモリーリソース
class malloc_resource : public std::pmr::memory_resource
{
public :
    //
    ~malloc_resource() { }
private :
    // ストレージの確保
    // 失敗した場合std::bad_allocをthrowする
    virtual void * 
    do_allocate( std::size_t bytes, std::size_t alignment ) override
    {
        void * ptr = std::malloc( bytes ) ;
        if ( ptr == nullptr )
        { throw std::bad_alloc{} ; }

        return ptr ;
    }

    // ストレージの解放
    virtual void 
    do_deallocate(  void * p, std::size_t bytes, 
                    std::size_t alignment ) override
    {
        std::free( p ) ;
    }

    virtual bool 
    do_is_equal( const memory_resource & other )
        const noexcept override
    {
        return dynamic_cast< const malloc_resource * >
                    ( &other ) != nullptr ;
    }

} ;

do_allocatemallocでストレージを確保し、do_deallocatefreeでストレージを解放する。メモリーリソースで0バイトのストレージを確保しようとしたときの規定はないので、mallocの挙動に任せる。mallocは0バイトのメモリーを確保しようとしたとき、C11では規定がない。POSIXではnullポインターを返すか、freeで解放可能な何らかのアドレスを返すものとしている。

do_is_equalは、malloc_resourceでさえあればどのオブジェクトから確保されたストレージであっても解放できるので、*thismalloc_resourceであるかどうかをdynamic_castで確認している。

polymorphic_allocator : 動的ポリモーフィズムを実現するアロケーター

std::pmr::polymorphic_allocatorはメモリーリソースを動的ポリモーフィズムとして振る舞うアロケーターにするためのライブラリだ。

従来のアロケーターは、静的ポリモーフィズムを実現するために設計されていた。たとえば独自のcustom_int_allocator型を使いたい場合は以下のように書く。

std::vector< int, custom_int_allocator > v ;

コンパイル時に使うべきアロケーターが決定できる場合はこれでいいのだが、実行時にアロケーターを選択したい場合、アロケーターをテンプレート引数に取る設計は問題になる。

そのため、C++17ではメモリーリソースをコンストラクター引数に取り、メモリーリソースからストレージを確保する実行時ポリモーフィックの振る舞いをするstd::pmr::polymorphic_allocatorが追加された。

たとえば、標準入力からtruefalseが入力されたかによって、システムのデフォルトのメモリーリソースと、monotonic_buffer_resourceを実行時に切り替えるには、以下のように書ける。

int main()
{
    bool b;

    std::cin >> b ;

    std::pmr::memory_resource * mem ;
    std::unique_ptr< memory_resource > mono ;

    if ( b )
    { // デフォルトのメモリーリソースを使う
        mem = std::pmr::get_default_resource() ;
    }
    else
    { // モノトニックバッファーを使う
        mono = std::make_unique< std::pmr::monotonic_buffer_resource >
                ( std::pmr::get_default_resource() ) ;
        mem = mono.get() ;
    }

    std::vector< int, std::pmr::polymorphic_allocator<int> >
        v( std::pmr::polymorphic_allocator<int>( mem ) ) ;
}

std::pmr::polymorphic_allocatorは以下のように宣言されている。

namespace std::pmr {

template <class T>
class polymorphic_allocator ;

}

テンプレート実引数にはstd::allocator<T>と同じく、確保する型を与える。

コンストラクター

polymorphic_allocator() noexcept;
polymorphic_allocator(memory_resource* r);

std::pmr::polymorphic_allocatorのデフォルトコンストラクターは、メモリーリソースをstd::pmr::get_default_resource()で取得する。

memory_resource *を引数に取るコンストラクターは、渡されたメモリーリソースをストレージ確保に使う。polymorphic_allocatorの生存期間中、メモリーリソースへのポインターは妥当なものでなければならない。

int main()
{
    // p1( std::pmr::get_default_resource () ) と同じ
    std::pmr::polymorphic_allocator<int> p1 ;

    std::pmr::polymorphic_allocator<int> p2(
        std::pmr::get_default_resource() ) ;
}

後は通常のアロケーターと同じように振る舞う。

プログラム全体で使われるメモリーリソースの取得

C++17では、プログラム全体で使われるメモリーリソースへのポインターを取得することができる。

new_delete_resource()

memory_resource* new_delete_resource() noexcept ;

関数new_delete_resourceはメモリーリソースへのポインターを返す。参照されるメモリーリソースは、ストレージの確保に::operator newを使い、ストレージの解放に::operator deleteを使う。

int main()
{
    auto mem = std::pmr::new_delete_resource() ;
}

null_memory_resource()

memory_resource* null_memory_resource() noexcept ;

関数null_memory_resourceはメモリーリソースへのポインターを返す。参照されるメモリーリソースのallocateは必ず失敗し、std::bad_allocthrowする。deallocateは何もしない。

このメモリーリソースは、ストレージの確保に失敗した場合のコードをテストする目的で使える。

デフォルトリソース

memory_resource* set_default_resource(memory_resource* r) noexcept ;
memory_resource* get_default_resource() noexcept ;

デフォルト・メモリーリソース・ポインターとは、メモリーリソースを明示的に指定することができない場合に、システムがデフォルトで利用するメモリーリソースへのポインターのことだ。初期値はnew_delete_resource()の戻り値となっている。

現在のデフォルト・メモリーリソース・ポインターと取得するためには、関数get_default_resourceを使う。デフォルト・メモリーリソース・ポインターを独自のメモリーリソースに差し替えるには、関数set_default_resourceを使う。

int main()
{
    // 現在のデフォルトのメモリーリソースへのポインター
    auto init_mem = std::pmr::get_default_resource() ;

    std::pmr::synchronized_pool_resource pool_mem ;

    // デフォルトのメモリーリソースを変更する
    std::pmr::set_default_resource( &pool_mem ) ;

    auto current_mem = std::pmr::get_default_resource() ;

    // true
    bool b = current_mem == pool_mem ;
}

標準ライブラリのメモリーリソース

標準ライブラリはメモリーリソースの実装として、プールリソースとモノトニックリソースを提供している。このメモリーリソースの詳細は後に解説するが、ここではそのための事前知識として、汎用的なメモリーアロケーター一般の解説をする。

プログラマーはメモリーを気軽に確保している。たとえば47バイトとか151バイトのような中途半端なサイズのメモリーを以下のように気軽に確保している。

int main()
{
    auto mem = std::get_default_resource() ;

    auto p1 = mem->allocate( 47 ) ;
    auto p2 = mem->allocate( 151 ) ;

    mem->deallocate( p1 ) ;
    mem->deallocate( p2 ) ;
}

しかし、残念ながら現実のハードウェアやOSのメモリー管理は、このように柔軟にはできていない。たとえば、あるアーキテクチャーとOSでは、メモリーはページサイズと呼ばれる単位でしか確保できない。そして最小のページサイズですら4Kバイトであったりする。もしシステムの低級なメモリー管理を使って上のコードを実装しようとすると、47バイト程度のメモリーを使うのに3Kバイト超の無駄が生じることになる。

他にもアライメントの問題がある。アーキテクチャーによってはメモリーアドレスが適切なアライメントに配置されていないとメモリーアクセスができないか、著しくパフォーマンスが落ちることがある。

mallocoperator newなどのメモリーアロケーターは、低級なメモリー管理を隠匿し、小さなサイズのメモリー確保を効率的に行うための実装をしている。

一般的には、大きな連続したアドレス空間のメモリーを確保し、その中に管理用のデータ構造を作り、メモリーを必要なサイズに切り出す。

// 実装イメージ

// ストレージを分割して管理するためのリンクリストデータ構造
struct alignas(std::max_align_t) chunk
{
    chunk * next ;
    chunk * prev ;
    std::size_t size ;
} ;

class memory_allocator : public std::pmr::memory_resource
{
    chunk * ptr ; // ストレージの先頭へのポインター
    std::size_t size ; // ストレージのサイズ
    std::mutex m ; // 同期用

    
public :

    memory_allocator()
    {
        // 大きな連続したストレージを確保
    }

    virtual void * 
    do_allocate( std::size_t bytes, std::size_t alignment ) override
    {
        std::scoped_lock lock( m ) ; 
        // リンクリストをたどり、十分な大きさの未使用領域を探し、リンクリスト構造体を
        // 構築して返す
        // アライメント要求に注意
    }

    virtual void * 
    do_deallocate( std::size_t bytes, std::size_t alignment ) override
    {
        std::scoped_lock lock( m ) ;
        // リンクリストから該当する部分を削除
    }

    virtual bool 
    do_is_equal( const memory_resource & other )
        const noexcept override
    { 
    // *thisとotherで相互にストレージを解放できるかどうか返す
    }
} ;

プールリソース

プールリソースはC++17の標準ライブラリが提供しているメモリーリソースの実装だ。synchronized_pool_resourceunsynchronized_pool_resourceの2つがある。

アルゴリズム

プールリソースは以下のような特徴を持つ。

void f()
{
    std::pmr::synchronized_pool_resource mem ;
    mem.allocate( 10 ) ;

    // 確保したストレージは破棄される
}
int main()
{
    // get_default_resource()が使われる
    std::pmr::synchronized_pool_resource m1 ;

    // 独自の上流メモリーリソースを指定
    custom_memory_resource mem ;
    std::pmr::synchronized_pool_resource m2( &mem ) ;
    
}
// 実装イメージ

namespace std::pmr {

// チャンクの実装
template < size_t block_size >
class chunk
{
    blocks<block_size> b ;
}

// プールの実装
template < size_t block_size >
class pool : public memory_resource
{
    chunks<block_size> c ;
} ;

class pool_resource : public memory_resource
{
    // それぞれのブロックサイズのプール
    pool<8> pool_8bytes ;
    pool<16> pool_16bytes ;
    pool<32> pool_32bytes ;

    // 上流メモリーリソース
    memory_resource * mem ;


    virtual void * do_allocate( size_t bytes, size_t alignment ) override
    {
        // 対応するブロックサイズのプールにディスパッチ
        if ( bytes <= 8 )
            return pool_8bytes.allocate( bytes, alignment ) ;
        else if ( bytes <= 16 )
            return pool_16bytes.allocate( bytes, alignment ) ;
        else if ( bytes < 32 )
            return pool_32bytes.allocate( bytes, alignment ) ;
        else
        // 最大ブロックサイズを超えたので上流メモリーリソースにディスパッチ
            return mem->allocate( bytes, alignment ) ;
    }
} ;

}

synchronized/unsynchronized_pool_resource

プールリソースには、synchronized_pool_resourceunsynchronized_pool_resourceがある。どちらもクラス名以外は同じように使える。ただし、synchronized_pool_resourceは複数のスレッドから同時に実行しても使えるように内部で同期が取られているのに対し、unsynchronized_pool_resourceは同期を行わない。unsynchronized_pool_resourceは複数のスレッドから同時に呼び出すことはできない。

// 実装イメージ

namespace std::pmr {

class synchronized_pool_resource : public memory_resource
{
    std::mutex m ;

    virtual void * 
    do_allocate( size_t size, size_t alignment ) override
    {
        // 同期する
        std::scoped_lock l(m) ;
        return do_allocate_impl( size, alignment ) ;
    }
} ;

class unsynchronized_pool_resource : public memory_resource
{
    virtual void * 
    do_allocate( size_t size, size_t alignment ) override
    {
        // 同期しない
        return do_allocate_impl( size, alignment ) ;
    }
} ;

}

pool_options

pool_optionsはプールリソースの挙動を指定するためのクラスで、以下のように定義されている。

namespace std::pmr {

struct pool_options {
    size_t max_blocks_per_chunk = 0;
    size_t largest_required_pool_block = 0;
};

}

このクラスのオブジェクトをプールリソースのコンストラクターに与えることで、プールリソースの挙動を指定できる。ただし、pool_optionsによる指定はあくまでも目安で、実装には従う義務はない。

max_blocks_per_chunkは、上流メモリーリソースからプールのチャンクを補充する際に一度に確保する最大のブロック数だ。この値がゼロか、実装の上限より大きい場合、実装の上限が使われる。実装は指定よりも小さい値を使うことができるし、またプールごとに別の値を使うこともできる。

largest_required_pool_blockはプール機構によって確保される最大のストレージのサイズだ。この値より大きなサイズのストレージを確保しようとすると、上流メモリーストレージから直接確保される。この値がゼロか、実装の上限よりも大きい場合、実装の上限が使われる。実装は指定よりも大きい値を使うこともできる。

プールリソースのコンストラクター

プールリソースの根本的なコンストラクターは以下のとおり。synchronizedunsynchronizedどちらも同じだ。

pool_resource(const pool_options& opts, memory_resource* upstream);

pool_resource()
: pool_resource(pool_options(), get_default_resource()) {}
explicit pool_resource(memory_resource* upstream)
: pool_resource(pool_options(), upstream) {}
explicit pool_resource(const pool_options& opts)
: pool_resource(opts, get_default_resource()) {}

pool_optionsmemory_resource *を指定する。指定しない場合はデフォルト値が使われる。

プールリソースのメンバー関数

release()

void release();

確保したストレージすべてを解放する。たとえ明示的にdeallocateを呼び出されていないストレージも解放する。

int main()
{
    synchronized_pool_resource mem ;
    void * ptr = mem.allocate( 10 ) ;

    // ptrは解放される
    mem.release() ;

}

upstream_resource()

memory_resource* upstream_resource() const;

構築時に渡した上流メモリーリソースへのポインターを返す。

options()

pool_options options() const;

構築時に渡したpool_optionsオブジェクトと同じ値を返す。

モノトニックバッファーリソース

モノトニックバッファーリソースはC++17で標準ライブラリに追加されたメモリーリソースの実装だ。クラス名はmonotonic_buffer_resource

モノトニックバッファーリソースは高速にメモリーを確保し、一気に解放するという用途に特化した特殊な設計をしている。モノトニックバッファーリソースはメモリー解放をせず、メモリー使用量がモノトニックに増え続けるので、この名前が付いている。

たとえばゲームで1フレームを描画する際に大量に小さなオブジェクトのためのストレージを確保し、その後確保したストレージをすべて解放したい場合を考える。通常のメモリーアロケーターでは、メモリー片を解放するためにメモリー全体に構築されたデータ構造をたどり、データ構造を書き換えなければならない。この処理は高くつく。すべてのメモリー片を一斉に解放してよいのであれば、データ構造をいちいちたどったり書き換えたりする必要はない。メモリーの管理は、単にポインターだけでよい。

// 実装イメージ

namespace std::pmr {

class monotonic_buffer_resource : public memory_resource
{
    // 連続した長大なストレージの先頭へのポインター
    void * ptr ;
    // 現在の未使用ストレージの先頭へのポインター
    std::byte * current ;

    virtual void * 
    do_allocate( size_t bytes, size_t alignment ) override
    {
        void * result = static_cast<void *>(current) ;
        current += bytes ; // 必要であればアライメント調整
        return result ;
    }

    virtual void 
    do_deallocate( void * ptr, size_t bytes, size_t alignment ) override 
    {
        // 何もしない
    }

public :
    ~monotonic_buffer_resource()
    {
        // ptrの解放
    }
} ;

}

このように、基本的な実装としては、do_allocateはポインターを加算して管理するだけだ。なぜならば解放処理がいらないため、個々のストレージ片を管理するためのデータ構造を構築する必要がない。do_deallocateは何もしない。デストラクターはストレージ全体を解放する。

アルゴリズム

モノトニックバッファーリソースは以下のような特徴を持つ。

int main()
{
    std::pmr::monotonic_buffer_resource mem ;

    void * ptr = mem.allocate( 10 ) ;
    // 何もしない
    // ストレージは解放されない
    mem.deallocate( ptr ) ;

    // memが破棄される際に確保したストレージはすべて破棄される
}
int main()
{
    std::byte initial_buffer[10] ;
    std::pmr::monotonic_buffer_resource 
        mem( initial_buffer, 10, std::pmr::get_default_resource() ) ;

    // 初期バッファーから確保
    mem.allocate( 1 ) ;
    // 上流メモリーリソースからストレージを確保して切り出して確保
    mem.allocate( 100 ) ;
    // 前回のストレージ確保で空きがあればそこから
    // なければ新たに上流から確保して切り出す
    mem.allocate( 100 ) ;
}

コンストラクター

モノトニックバッファーリソースには以下のコンストラクターがある。

explicit monotonic_buffer_resource(memory_resource *upstream);
monotonic_buffer_resource(  size_t initial_size,
                            memory_resource *upstream);
monotonic_buffer_resource(  void *buffer, size_t buffer_size,
                            memory_resource *upstream);


monotonic_buffer_resource()
    : monotonic_buffer_resource(get_default_resource()) {}
explicit monotonic_buffer_resource(size_t initial_size)
    : monotonic_buffer_resource(initial_size,
                                get_default_resource()) {}
monotonic_buffer_resource(void *buffer, size_t buffer_size)
    : monotonic_buffer_resource(buffer, buffer_size,
                                get_default_resource()) {}

初期バッファーを取らないコンストラクターは以下のとおり。

explicit monotonic_buffer_resource(memory_resource *upstream);
monotonic_buffer_resource(  size_t initial_size,
                            memory_resource *upstream);

monotonic_buffer_resource()
    : monotonic_buffer_resource(get_default_resource()) {}
explicit monotonic_buffer_resource(size_t initial_size)
    : monotonic_buffer_resource(initial_size,
                                get_default_resource()) {}

initial_sizeは、上流メモリーリソースから最初に確保するバッファーのサイズ(初期サイズ)のヒントとなる。実装はこのサイズか、あるいは実装依存のサイズをバッファーとして確保する。

デフォルトコンストラクターは上流メモリーリソースにstd::pmr_get_default_resource()を与えたのと同じ挙動になる。

size_t1つだけを取るコンストラクターは、初期サイズだけを与えて後はデフォルトの扱いになる。

初期バッファーを取るコンストラクターは以下のとおり。

monotonic_buffer_resource(  void *buffer, size_t buffer_size,
                            memory_resource *upstream);

monotonic_buffer_resource(void *buffer, size_t buffer_size)
    : monotonic_buffer_resource(buffer, buffer_size,
                                get_default_resource()) {}

初期バッファーは先頭アドレスをvoid *型で渡し、そのサイズをsize_t型で渡す。

その他の操作

release()

void release() ;

メンバー関数releaseは、上流リソースから確保されたストレージをすべて解放する。明示的にdeallocateを呼び出していないストレージも解放される。

int main()
{
    std::pmr::monotonic_buffer_resource mem ;

    mem.allocate( 10 ) ;

    // ストレージはすべて解放される
    mem.release() ;

}

upstream_resource()

memory_resource* upstream_resource() const;

メンバー関数upstream_resourceは、構築時に与えられた上流メモリーリソースへのポインターを返す。

並列アルゴリズム

並列アルゴリズムはC++17で追加された新しいライブラリだ。このライブラリは既存の<algorithm>に、並列実行版を追加する。

並列実行について

C++11では、スレッドと同期処理が追加され、複数の実行媒体が同時に実行されるという概念がC++標準規格に入った。

C++17では、既存のアルゴリズムに、並列実行版が追加された。

たとえば、all_of(first, last, pred)というアルゴリズムは、[first,last)の区間が空であるか、すべてのイテレーターiに対してpred(*i)trueを返すとき、trueを返す。それ以外の場合はfalseを返す。

すべての値が100未満であるかどうかを調べるには、以下のように書く。

template < typename Container >
bool is_all_of_less_than_100( Container const & input )
{
    return std::all_of( std::begin(input), std::end(input),
        []( auto x ) { return x < 100 ; } ) ;
}

int main()
{
    std::vector<int> input ;
    std::copy( std::istream_iterator<int>(std::cin),
        std::istream_iterator<int>(), std::back_inserter(input) ) ;

    bool result = is_all_of_less_than_100( input ) ;

    std::cout << "result : " << result << std::endl ;
}

本書の執筆時点では、コンピューターはマルチコアが一般的になり、同時に複数のスレッドを実行できるようになった。さっそくこの処理を2つのスレッドで並列化してみよう。

template < typename Container >
bool double_is_all_of_less_than_100( Container const & input )
{
    auto first = std::begin(input) ;
    auto last = first + (input.size()/2) ;

    auto r1 = std::async( [=]
    {
        return std::all_of( first, last,
                            [](auto x) { return x < 100 ; } ) ; 
    } ) ;

    first = last ;
    last = std::end(input) ;

    auto r2 = std::async( [=]
    {
        return std::all_of( first, last,
                            [](auto x) { return x < 100 ; } ) ;
    } ) ;

    return r1.get() && r2.get() ;
}

なるほど、とてもわかりにくいコードだ。

筆者のコンピューターのCPUは2つの物理コア、4つの論理コアを持っているので、4スレッドまで同時に並列実行できる。読者の使っているコンピューターは、より高性能でさらに多くのスレッドを同時に実行可能だろう。実行時に最大の効率を出すようにできるだけ頑張ってみよう。

template < typename Container >
bool parallel_is_all_of_less_than_100( Container const & input )
{
    std::size_t cores = std::thread::hardware_concurrency() ;
    cores = std::min( input.size(), cores ) ;

    std::vector< std::future<bool> > futures( cores ) ;

    auto step = input.size() / cores ;
    auto remainder = input.size() % cores ;

    auto first = std::begin(input) ;
    auto last = first + step + remainder ;

    for ( auto & f : futures )
    {
        f = std::async( [=]
        {
            return std::all_of( first, last,
                                [](auto x){ return x < 100 ; } ) ;
        } ) ;

        first = last ;
        last = first + step ;
    }

    for ( auto & f : futures )
    {
        if ( f.get() == false )
            return false ;
    }
    return true ;
}

もうわけがわからない。

このような並列化をそれぞれのアルゴリズムに対して自前で実装するのは面倒だ。そこで、C++17では標準で並列実行してくれる並列アルゴリズム(Parallelism)が追加された。

使い方

並列アルゴリズムは既存のアルゴリズムのオーバーロードとして追加されている。

以下は既存のアルゴリズムであるall_ofの宣言だ。

template <class InputIterator, class Predicate>
bool all_of(InputIterator first, InputIterator last, Predicate pred);

並列アルゴリズム版のall_ofは以下のような宣言になる。

template <  class ExecutionPolicy, class ForwardIterator,
            class Predicate>
bool all_of(ExecutionPolicy&& exec, ForwardIterator first,
            ForwardIterator last, Predicate pred);

並列アルゴリズムには、テンプレート仮引数としてExecutionPolicyが追加されていて第一引数に取る。これを実行時ポリシーと呼ぶ。

実行時ポリシーは<execution>で定義されている関数ディスパッチ用のタグ型で、std::execution::seq, std::execution::par, std::execution::par_unseqがある。

複数のスレッドによる並列実行を行うには、std::execution::parを使う。

template < typename Container >
bool is_all_of_less_than_100( Container const & input )
{
    return std::all_of( std::execution::par,
        std::begin(input), std::end(input),
        []( auto x ){ return x < 100 ; } ) ;
}

std::execution::seqを渡すと既存のアルゴリズムと同じシーケンシャル実行になる。std::execution::parを渡すとパラレル実行になる。std::execution::par_unseqは並列実行かつベクトル実行になる。

C++17には実行ポリシーを受け取るアルゴリズムのオーバーロード関数が追加されている。

並列アルゴリズム詳細

並列アルゴリズム

並列アルゴリズム(parallel algorithm)とは、ExecutionPolicy(実行ポリシー)というテンプレートパラメーターのある関数テンプレートのことだ。既存の<algorithm>とC++14で追加された一部の関数テンプレートが、並列アルゴリズムに対応している。

並列アルゴリズムはイテレーター、仕様上定められた操作、ユーザーの提供する関数オブジェクトによる操作、仕様上定められた関数オブジェクトに対する操作によって、オブジェクトにアクセスする。そのような関数群を、要素アクセス関数(element access functions)と呼ぶ。

たとえば、std::sortは以下のような要素アクセス関数を持つ。

並列アルゴリズムが使う要素アクセス関数は、並列実行に伴うさまざまな制約を満たさなければならない。

ユーザー提供する関数オブジェクトの制約

並列アルゴリズムのうち、テンプレートパラメーター名が、Predicate, BinaryPredicate, Compare, UnaryOperation, BinaryOperation, BinaryOperation1, BinaryOperation2となっているものは、関数オブジェクトとしてユーザーがアルゴリズムに提供するものである。このようなユーザー提供の関数オブジェクトには、並列アルゴリズムに渡す際の制約がある。

一部の特殊なアルゴリズムには例外もあるが、ほとんどの並列アルゴリズムではこの制約を満たさなければならない。

実引数で与えられたオブジェクトを直接、間接に変更してはならない

ユーザー提供の関数オブジェクトは実引数で与えられたオブジェクトを直接、間接に変更してはならない。

つまり、以下のようなコードは違法だ。

int main()
{
    std::vector<int> c = { 1,2,3,4,5 } ;
    std::all_of( std::execution::par, std::begin(c), std::end(c),
        [](auto & x){ ++x ; return true ; } ) ;
    // エラー
}

これは、ユーザー提供の関数オブジェクトが実引数をlvalueリファレンスで受け取って変更しているので、並列アルゴリズムの制約を満たさない。

std::for_eachはイテレーターが変更可能な要素を返す場合、ユーザー提供の関数オブジェクトが実引数を変更することが可能だ。

int main()
{
    std::vector<int> c = { 1,2,3,4,5 } ;
    std::for_each( std::execution::par, std::begin(c), std::end(c),
        [](auto & x){ ++x ; } ) ;
    // OK
}

これは、for_eachは仕様上そのように定められているからだ。

実引数で与えられたオブジェクトの一意性に依存してはならない

ユーザー提供の関数オブジェクトは実引数で与えられたオブジェクトの一意性に依存してはならない。

これはどういうことかというと、たとえば実引数で渡されたオブジェクトのアドレスを取得して、そのアドレスがアルゴリズムに渡したオブジェクトのアドレスと同じであることを期待するようなコードを書くことができない。

int main()
{
    std::vector<int> c = { 1,2,3,4,5 } ;

    // 最後の要素へのポインター
    int * ptr = &c[4] ;

    std::all_of( std::execution::par, std::begin(c), std::end(c),
        [=]( auto & x ){
            if ( ptr == &x )
            {
                // 最後の要素なので特別な処理
                // エラー
            }
        } ) ;
}

これはなぜかというと、並列アルゴリズムはその並列処理の一環として、要素のコピーを作成し、そのコピーをユーザー提供の関数オブジェクトに渡すかもしれないからだ。

// 実装イメージ

template <  typename ExecutionPolicy,
            typename ForwardIterator,
            typename Predicate >
bool all_of(    ExecutionPolicy && exec,
                ForwardIterator first, ForwardIterator last,
                Predicate pred )
{
    if constexpr (
        std::is_same_v< ExecutionPolicy,
                        std::execution::parallel_policy >
    )
    {
        std::vector c( first, last ) ;
        do_all_of_par( std::begin(c), std::end(c), pred ) ;
    }
}

このため、オブジェクトの一意性に依存したコードを書くことはできない。

データ競合と同期

std::execution::sequenced_policyを渡した並列アルゴリズムによる要素アクセス関数の呼び出しは呼び出し側スレッドで実行される。パラレル実行ではない。

std::execution::parallel_policyを渡した並列アルゴリズムによる要素アクセス関数の呼び出しは、呼び出し側スレッドか、ライブラリ側で作られたスレッドのいずれかで実行される。それぞれの要素アクセス関数の呼び出しの同期は定められていない。そのため、要素アクセス関数はデータ競合やデッドロックを起こさないようにしなければならない。

以下のコードはデータ競合が発生するのでエラーとなる。

int main()
{
    int sum = 0 ;

    std::vector<int> c = { 1,2,3,4,5 } ;

    std::for_each( std::execution::par, std::begin(c), std::end(c),
        [&]( auto x ){ sum += x ; } ) ;
    // エラー、データ競合
}

なぜならば、ユーザー提供の関数オブジェクトは複数のスレッドから同時に呼び出されるかもしれないからだ。

std::execution::parallel_unsequenced_policyの実行は変わっている。未規定のスレッドから同期されない実行が許されている。これは、パラレルベクトル実行で想定している実行媒体がスレッドのような強い実行保証のある実行媒体ではなく、SIMDやGPGPUのような極めて軽い実行媒体であるからだ。

その結果、要素アクセス関数は通常のデータ競合やデッドロックを防ぐための手段すら取れなくなる。なぜならば、スレッドは実行の途中に中断して別の処理をしたりするからだ。

たとえば、以下のコードは動かない。

int main()
{
    int sum = 0 ;
    std::mutex m ;

    std::vector<int> c = { 1,2,3,4,5 } ;

    std::for_each(
        std::execution::par_unseq,
        std::begin(c), std::end(c),
        [&]( auto x ) {
            std::scoped_lock l(m) ;
            sum += x ; 
        } ) ;
    // エラー
}

このコードはparallel_policyならば、非効率的ではあるが問題なく同期されてデータ競合なく動くコードだ。しかし、parallel_unsequenced_policyでは動かない。なぜならば、mutexlockという同期をする関数を呼び出すからだ。

C++では、ストレージの確保解放以外の同期する標準ライブラリの関数をすべて、ベクトル化非安全(vectorization-unsafe)に分類している。ベクトル化非安全な関数はstd::execution::parallel_unsequenced_policyの要素アクセス関数内で呼び出すことはできない。

例外

並列アルゴリズムの実行中に、一時メモリーの確保が必要になったが確保できない場合、std::bad_allocがthrowされる。

並列アルゴリズムの実行中に、要素アクセス関数の外に例外が投げられた場合、std::terminateが呼ばれる。

実行ポリシー

実行ポリシーはヘッダーファイル<execution>で定義されている。その定義は以下のようになっている。

namespace std {
template<class T> struct is_execution_policy;
template<class T> inline constexpr bool
    is_execution_policy_v = is_execution_policy<T>::value;
}

namespace std::execution {

class sequenced_policy;
class parallel_policy;
class parallel_unsequenced_policy;

inline constexpr sequenced_policy seq{ };
inline constexpr parallel_policy par{ };
inline constexpr parallel_unsequenced_policy par_unseq{ };

}

is_execution_policy traits

std::is_execution_policy<T>Tが実行ポリシー型であるかどうかを返すtraitsだ。

// false
constexpr bool b1 = std::is_execution_policy_v<int> ;
// true
constexpr bool b2 = 
    std::is_execution_policy_v<std::execution::sequenced_policy> ;

シーケンス実行ポリシー

namespace std::execution {

class sequenced_policy ;
inline constexpr sequenced_policy seq { } ;

}

シーケンス実行ポリシーは、並列アルゴリズムにパラレル実行を行わせないためのポリシーだ。この実行ポリシーが渡された場合、処理は呼び出し元のスレッドだけで行われる。

パラレル実行ポリシー

namespace std::execution {

class parallel_policy ;
inline constexpr parallel_policy par { } ;

}

パラレル実行ポリシーは、並列アルゴリズムにパラレル実行を行わせるためのポリシーだ。この実行ポリシーが渡された場合、処理は呼び出し元のスレッドと、ライブラリが作成したスレッドを用いる。

パラレル非シーケンス実行ポリシー

namespace std::execution {

class parallel_unsequenced_policy ;
inline constexpr parallel_unsequenced_policy par_unseq { } ;

}

パラレル非シーケンス実行ポリシーは、並列アルゴリズムにパラレル実行かつベクトル実行を行わせるためのポリシーだ。この実行ポリシーが渡された場合、処理は複数のスレッドと、SIMDやGPGPUのようなベクトル実行による並列化を行う。

実行ポリシーオブジェクト

namespace std::execution {

inline constexpr sequenced_policy seq{ };
inline constexpr parallel_policy par{ };
inline constexpr parallel_unsequenced_policy par_unseq{ };

}

実行ポリシーの型を直接書くのは面倒だ。

std::for_each( std::execution::parallel_policy{}, ... ) ;

そのため、標準ライブラリは実行ポリシーのオブジェクトを用意している。seqparpar_unseqがある。

std::for_each( std::execution::par, ... ) ;

並列アルゴリズムを使うには、このオブジェクトを並列アルゴリズムの第一引数に渡すことになる。

数学の特殊関数群

C++17では数学の特殊関数群(mathematical special functions)がヘッダーファイル<cmath>に追加された。

数学の特殊関数は、いずれも実引数を取って、規定の計算をし、結果を浮動小数点数型の戻り値として返す。

数学の特殊関数はdouble, float, long double型の3つのオーバーロードがある。それぞれ、関数名の最後に、何もなし、f, lというサフィックスで表現されている。

double      function_name() ;   // 何もなし
float       function_namef() ;  // f
long double function_namel() ;  // l

数学の特殊関数の説明は、関数の宣言、効果、戻り値、注意がある。

もし、数学の特殊関数に渡した実引数がNaN(Not a Number)である場合、関数の戻り値もNaNになる。ただし定義域エラーは起こらない。

それ以外の場合で、関数が定義域エラーを返すべきときは、

別途示されていない場合、関数はすべての有限の値、負の無限大、正の無限大に対しても定義されている。

数学関数が与えられた実引数の値に対して定義されているというとき、それは以下のいずれかである。

ある関数の効果が実装定義(implementation-defined)である場合、その効果はC++標準規格で定義されず、C++実装はどのように実装してもよいという意味だ。

ラゲール多項式(Laguerre polynomials)

double       laguerre(unsigned n, double x);
float        laguerref(unsigned n, float x);
long double  laguerrel(unsigned n, long double x);

効果:実引数n, xに対するラゲール多項式(Laguerre polynomials)を計算する。

戻り値

\[ \mathsf{L}_n(x) = \frac{e^x}{n!} \frac{ \mathsf{d} ^ n} { \mathsf{d}x ^ n} \, (x^n e^{-x}), \quad \mbox{for $x \ge 0$} \]

\(n\)n, \(x\)xとする。

注意n >= 128のときの関数の呼び出しの効果は実装定義である。

ラゲール陪多項式(Associated Laguerre polynomials)

double      assoc_laguerre(unsigned n, unsigned m, double x);
float       assoc_laguerref(unsigned n, unsigned m, float x);
long double assoc_laguerrel(unsigned n, unsigned m, long double x);

効果:実引数n, m, xに対するラゲール陪多項式(Associated Laguerre polynomials)を計算する。

戻り値

\[ \mathsf{L}_n^m(x) = (-1)^m \frac{\mathsf{d} ^ m} {\mathsf{d}x ^ m} \, \mathsf{L}_{n+m}(x), \quad \mbox{for $x \ge 0$} \]

\(n\)n, \(m\)m, \(x\)xとする。

注意n >= 128 もしくは m >= 128 のときの関数呼び出しの効果は実装定義である。

ルジャンドル多項式(Legendre polynomials)

double       legendre(unsigned l, double x);
float        legendref(unsigned l, float x);
long double  legendrel(unsigned l, long double x);

効果:実引数l, xに対するルジャンドル多項式(Legendre polynomials)を計算する。

戻り値

\[ \mathsf{P}_\ell(x) = \frac{1} {2^\ell \, \ell!} \frac{ \mathsf{d} ^ \ell} { \mathsf{d}x ^ \ell} \, (x^2 - 1) ^ \ell, \quad \mbox{for $|x| \le 1$} \]

\(l\)l, \(x\)xとする。

注意l >= 128 のときの関数の呼び出しの効果は実装定義である。

ルジャンドル陪関数(Associated Legendre functions)

double      assoc_legendre(unsigned l, unsigned m, double x);
float       assoc_legendref(unsigned l, unsigned m, float x);
long double assoc_legendrel(unsigned l, unsigned m, long double x);

効果:実引数l, m, xに対するルジャンドル陪関数(Associated Legendre functions)を計算する。

戻り値

\[ \mathsf{P}_\ell^m(x) = (1 - x^2) ^ {m/2} \: \frac{ \mathsf{d} ^ m} { \mathsf{d}x ^ m} \, \mathsf{P}_\ell(x), \quad \mbox{for $|x| \le 1$} \]

\(l\)l, \(m\)m, \(x\)xとする。

注意l >= 128 のときの関数呼び出しの効果は実装定義である。

球面ルジャンドル陪関数(Spherical associated Legendre functions)

double       sph_legendre(  unsigned l, unsigned m, double theta);
float        sph_legendref( unsigned l, unsigned m, float theta);
long double  sph_legendrel( unsigned l, unsigned m,
                            long double theta);

効果:実引数l, m, thetathetaの単位はラジアン)に対する球面ルジャンドル陪関数(Spherical associated Legendre functions)を計算する。

戻り値

\[ \mathsf{Y}_\ell^m(\theta, 0) \]

このとき、

\[ \mathsf{Y}_\ell^m(\theta, \phi) = (-1)^m \left[ \frac{(2 \ell + 1)} {4 \pi} \frac{(\ell - m)!} {(\ell + m)!} \right]^{1/2} \mathsf{P}_\ell^m ( \cos\theta ) e ^ {i m \phi}, \quad \mbox{for $|m| \le \ell$} \]

\(l\)l, \(m\)m, \(\theta\)thetaとする。

注意l >= 128 のときの関数の呼び出しの効果は実装定義である。

球面調和関数(Spherical harmonics) \(\mathsf{Y}_\ell^m(\theta, \phi)\) は、以下のような関数を定義することによって計算できる。

#include <cmath>
#include <complex>

std::complex<double>
spherical_harmonics(unsigned l, unsigned m, double theta, double phi)
{
    return std::sph_legendre(l, m, theta) * std::polar(1.0, m * phi) ;
}

ルジャンドル陪関数も参照。

エルミート多項式(Hermite polynomials)

double       hermite(unsigned n, double x);
float        hermitef(unsigned n, float x);
long double  hermitel(unsigned n, long double x);

効果:実引数n, xに対するエルミート多項式(Hermite polynomials)を計算する。

戻り値

\[ \mathsf{H}_n(x) = (-1)^n e^{x^2} \frac{ \mathsf{d} ^n} { \mathsf{d}x^n} \, e^{-x^2} \; \]

\(n\)n, \(x\)xとする。

注意n >= 128 のときの関数の呼び出しの効果は実装定義である。

ベータ関数(Beta function)

double      beta(double x, double y);
float       betaf(float x, float y);
long double betal(long double x, long double y);

効果:実引数x, yに対するベータ関数(Beta function)を計算する。

戻り値

\[ \mathsf{B}(x, y) = \frac{ \Gamma(x) \, \Gamma(y) } { \Gamma(x+y) }, \quad \mbox{for $x > 0$,\, $y > 0$} \]

\(x\)x, \(y\)yとする。

第1種完全楕円積分(Complete elliptic integral of the first kind)

double      comp_ellint_1(double k);
float       comp_ellint_1f(float k);
long double comp_ellint_1l(long double k);

効果:実引数kに対する第1種完全楕円積分(Complete elliptic integral of the first kind)を計算する。

戻り値

\[ \mathsf{K}(k) = \mathsf{F}(k, \pi / 2), \quad \mbox{for $|k| \le 1$} \]

\(k\)kとする。

第1種不完全楕円積分も参照。

第2種完全楕円積分(Complete elliptic integral of the second kind)

double      comp_ellint_2(double k);
float       comp_ellint_2f(float k);
long double comp_ellint_2l(long double k);

効果:実引数kに対する第2種完全楕円積分(Complete elliptic integral of the second kind)を計算する。

戻り値

\[ \mathsf{E}(k) = \mathsf{E}(k, \pi / 2), \quad \mbox{for $|k| \le 1$} \]

\(k\)kとする。

第2種不完全楕円積分も参照。

第3種完全楕円積分(Complete elliptic integral of the third kind)

double      comp_ellint_3(double k, double nu);
float       comp_ellint_3f(float k, float nu);
long double comp_ellint_3l(long double k, long double nu);

効果:実引数k, nuに対する第3種完全楕円積分(Complete elliptic integral of the third kind)を計算する。

戻り値

\[ \mathsf{\Pi}(\nu, k) = \mathsf{\Pi}(\nu, k, \pi / 2), \quad \mbox{for $|k| \le 1$} \]

\(k\)k, \(\nu\)nuとする。

第3種不完全楕円積分も参照。

第1種不完全楕円積分(Incomplete elliptic integral of the first kind)

double       ellint_1(double k, double phi);
float        ellint_1f(float k, float phi);
long double  ellint_1l(long double k, long double phi);

効果:実引数k, phiphiの単位はラジアン)に対する第1種不完全楕円積分(Incomplete elliptic integral of the first kind)を計算する。

戻り値

\[ \mathsf{F}(k, \phi) = \int_0^\phi \! \frac{\mathsf{d}\theta} {\sqrt{1 - k^2 \sin^2 \theta}}, \quad \mbox{for $|k| \le 1$} \]

\(k\)k, \(\phi\)phiとする。

第2種不完全楕円積分(Incomplete elliptic integroal of the second kind)

double       ellint_2(double k, double phi);
float        ellint_2f(float k, float phi);
long double  ellint_2l(long double k, long double phi);

効果:実引数k, phiphiの単位はラジアン)に対する第2種不完全楕円積分(Incomplete elliptic integral of the second kind)を計算する。

戻り値

\[ \mathsf{E}(k, \phi) = \int_0^\phi \! \sqrt{1 - k^2 \sin^2 \theta} \, \mathsf{d}\theta, \quad \mbox{for $|k| \le 1$} \]

\(k\)k, \(\phi\)phiとする。

第3種不完全楕円積分(Incomplete elliptic integral of the third kind)

double       ellint_3(  double k, double nu, double phi);
float        ellint_3f( float k, float nu, float phi);
long double  ellint_3l( long double k, long double nu,
                        long double phi);

効果:実引数k, nu, phiphiの単位はラジアン)に対する第3種不完全楕円積分(Incomplete elliptic integral of the third kind)を計算する。

戻り値

\[ \mathsf{\Pi}(\nu, k, \phi) = \int_0^\phi \! \frac{ \mathsf{d}\theta } { (1 - \nu \, \sin^2 \theta) \sqrt{1 - k^2 \sin^2 \theta} }, \quad \mbox{for $|k| \le 1$} \]

\(\nu\)nu, \(k\)k, \(\phi\)phiとする。

第1種ベッセル関数(Cylindrical Bessel functions of the first kind)

double       cyl_bessel_j(double nu, double x);
float        cyl_bessel_jf(float nu, float x);
long double  cyl_bessel_jl(long double nu, long double x);

効果:実引数nu, kに対する第1種ベッセル関数(Cylindrical Bessel functions of the first kind, Bessel functions of the first kind)を計算する。

戻り値

\[ \mathsf{J}_\nu(x) = \sum_{k=0}^\infty \frac{(-1)^k (x/2)^{\nu+2k}} {k! \: \Gamma(\nu+k+1)}, \quad \mbox{for $x \ge 0$} \]

\(\nu\)nu, \(x\)xとする。

注意nu >= 128 のときの関数の呼び出しの効果は実装定義である。

ノイマン関数(Cylindrical Neumann functions)

double       cyl_neumann(double nu, double x);
float        cyl_neumannf(float nu, float x);
long double  cyl_neumannl(long double nu, long double x);

効果:実引数nu, xに対するノイマン関数(Cylindrical Neumann functions, Neumann functions)、またの名を第2種ベッセル関数(Cylindrical Bessel functions of the second kind, Bessel functions of the second kind)を計算する。

戻り値

\[ \mathsf{N}_\nu(x) = \left\{ \begin{array}{cl} \displaystyle \frac{\mathsf{J}_\nu(x) \cos \nu\pi - \mathsf{J}_{-\nu}(x)} {\sin \nu\pi }, & \mbox{for $x \ge 0$ and non-integral $\nu$} \\ \\ \displaystyle \lim_{\mu \rightarrow \nu} \frac{\mathsf{J}_\mu(x) \cos \mu\pi - \mathsf{J}_{-\mu}(x)} {\sin \mu\pi }, & \mbox{for $x \ge 0$ and integral $\nu$} \end{array} \right. \]

\(\nu\)nu, \(x\)xとする。

注意nu >= 128 のときの関数の呼び出しの効果は実装定義である。

第1種ベッセル関数も参照。

第1種変形ベッセル関数(Regular modified cylindrical Bessel functions)

double       cyl_bessel_i(double nu, double x);
float        cyl_bessel_if(float nu, float x);
long double  cyl_bessel_il(long double nu, long double x);

効果:実引数nu, xに対する第1種変形ベッセル関数(Regular modified cylindrical Bessel functions, Modified Bessel functions of the first kind)を計算する。

戻り値

\[ \mathsf{I}_\nu(x) = \mathrm{i}^{-\nu} \mathsf{J}_\nu(\mathrm{i}x) = \sum_{k=0}^\infty \frac{(x/2)^{\nu+2k}} {k! \: \Gamma(\nu+k+1)}, \quad \mbox{for $x \ge 0$} \]

\(\nu\)nu, \(x\)xとする。

注意nu >= 128 のときの関数の呼び出しの効果は実装定義である。

第1種ベッセル関数も参照。

第2種変形ベッセル関数(Irregular modified cylindrical Bessel functions)

double       cyl_bessel_k(double nu, double x);
float        cyl_bessel_kf(float nu, float x);
long double  cyl_bessel_kl(long double nu, long double x);

効果:実引数nu, xに対する第2種変形ベッセル関数(Irregular modified cylindrical Bessel functions, Modified Bessel functions of the second kind)を計算する。

戻り値

\[ \mathsf{K}_\nu(x) = (\pi/2)\mathrm{i}^{\nu+1} ( \mathsf{J}_\nu(\mathrm{i}x) + \mathrm{i} \mathsf{N}_\nu(\mathrm{i}x) ) = \left\{ \begin{array}{cl} \displaystyle \frac{\pi}{2} \frac{\mathsf{I}_{-\nu}(x) - \mathsf{I}_{\nu}(x)} {\sin \nu\pi }, & \mbox{for $x \ge 0$ and non-integral $\nu$} \\ \\ \displaystyle \frac{\pi}{2} \lim_{\mu \rightarrow \nu} \frac{\mathsf{I}_{-\mu}(x) - \mathsf{I}_{\mu}(x)} {\sin \mu\pi }, & \mbox{for $x \ge 0$ and integral $\nu$} \end{array} \right. \]

\(\nu\)nu, \(x\)xとする。

注意nu >= 128 のときの関数の呼び出しの効果は実装定義である。

第1種変形ベッセル関数第1種ベッセル関数ノイマン関数も参照。

第1種球ベッセル関数(Spherical Bessel functions of the first kind)

double       sph_bessel(unsigned n, double x);
float        sph_besself(unsigned n, float x);
long double  sph_bessell(unsigned n, long double x);

効果:実引数n, xに対する第1種球ベッセル関数(Spherical Bessel functions of the first kind)を計算する。

戻り値

\[ \mathsf{j}_n(x) = (\pi/2x)^{1\!/\!2} \mathsf{J}_{n + 1\!/\!2}(x), \quad \mbox{for $x \ge 0$} \]

注意n >= 128 のときの関数の呼び出しの効果は実装定義である。

第1種ベッセル関数も参照。

球ノイマン関数(Spherical Neumann functions)

double       sph_neumann(unsigned n, double x);
float        sph_neumannf(unsigned n, float x);
long double  sph_neumannl(unsigned n, long double x);

効果:実引数n, xに対する球ノイマン関数(Spherical Neumann functions)、またの名を第2種球ベッセル関数(Spherical Bessel functions of the second kind)を計算する。

戻り値

\[ \mathsf{n}_n(x) = (\pi/2x)^{1\!/\!2} \mathsf{N}_{n + 1\!/\!2}(x), \quad \mbox{for $x \ge 0$} \]

\(n\)n, \(x\)xとする。

注意n >= 128 のときの関数の呼び出しの効果は実装定義である。

ノイマン関数も参照。

指数積分(Exponential integral)

double       expint(double x);
float        expintf(float x);
long double  expintl(long double x);

効果:実引数xに対する指数積分(Exponential integral)を計算する。

戻り値

\[ \mathsf{Ei}(x) = - \int_{-x}^\infty \frac{e^{-t}} {t } \, \mathsf{d}t \; \]

\(x\)xとする。

リーマンゼータ関数(Riemann zeta function)

double       riemann_zeta(double x);
float        riemann_zetaf(float x);
long double  riemann_zetal(long double x);

効果:実引数xに対するリーマンゼータ関数(Riemann zeta function)を計算する。

戻り値

\[ \mathsf{\zeta}(x) = \left\{ \begin{array}{cl} \displaystyle \sum_{k=1}^\infty k^{-x}, & \mbox{for $x > 1$} \\ \\ \displaystyle \frac{1} {1 - 2^{1-x}} \sum_{k=1}^\infty (-1)^{k-1} k^{-x}, & \mbox{for $0 \le x \le 1$} \\ \\ \displaystyle 2^x \pi^{x-1} \sin(\frac{\pi x}{2}) \, \Gamma(1-x) \, \zeta(1-x), & \mbox{for $x < 0$} \end{array} \right. \; \]

\(x\)xとする。

その他の標準ライブラリ

この章ではC++17で追加された細かいライブラリをまとめて解説する。

ハードウェア干渉サイズ(キャッシュライン)

C++17にはハードウェアの干渉サイズを取得するライブラリが入った。ハードウェアの干渉サイズとは、俗にキャッシュライン(cache line)とも呼ばれている概念だ。

残念ながら、この2017年では、メモリーは極めて遅い。そのため、プロセッサーはより高速にアクセスできるキャッシュメモリーを用意している。メモリーに対するキャッシュはある程度のまとまったバイト数単位で行われる。この単位が何バイトであるのかは実装依存だ。C++17にはこのサイズを取得できるライブラリが入った。

ハードウェア干渉サイズを知りたい理由は2つある。2つのオブジェクトを同一の局所性を持つキャッシュに載せたくない場合と載せたい場合だ。

2つのオブジェクトのうち、一方は頻繁に変更し、もう一方はめったに変更しない場合で、2つのオブジェクトが同じ局所性を持つキャッシュに載っている場合、よく変更するオブジェクトを変更しただけで、めったに変更しないオブジェクトも、メモリーとの同期が発生する。

struct Data
{
    int counter ;
    int status ;
} ;

ここで、counterは頻繁に変更するが、statusはめったに変更しない場合、counterstatusの間に適切なパディングを挿入することで、2つのオブジェクトが同じ局所性を持たないようにしたい。

この場合には、std::hardware_destructive_interference_sizeが使える。

struct Data
{
    int counter ;
    std::byte padding[
        std::hardware_destructive_interference_size - sizeof(int)
    ] ;
    int status ;
} ;

反対に、2つのオブジェクトを同一の局所性を持つキャッシュに載せたい場合、std::hardware_constructive_interference_sizeが使える。

ハードウェア干渉サイズは<new>ヘッダーで以下のように定義されている。

namespace std {
    inline constexpr size_t
        hardware_destructive_interference_size = 実装依存 ;
    inline constexpr size_t
        hardware_constructive_interference_size = 実装依存 ;
}

std::uncaught_exceptions

C++14までは、まだcatchされていない例外がある場合は、bool std::uncaught_exception()で判定することができた。

struct X
{
    ~X()
    {
        if ( std::uncaught_exception() )
        {
            // デストラクターはスタックアンワインディング中に呼ばれた
        }
        else
        {
            // 通常の破棄
        }
    }
} ;

int main()
{
    {
        X x ;
    }// 通常の破棄

    {
        X x ;
        throw 0 ;
    }// スタックアンワインディング中

}

bool std::uncaught_exception()は、C++17では非推奨扱いになった。いずれ廃止される見込みだ。

廃止の理由としては、単に以下のような例で役に立たないからだ。

struct X
{
    ~X()
    {
        try {
            // true
            bool b = std::uncaught_exception() ;
        } catch( ... ) { }
    }
} ;

このため、int std::uncaught_exceptions()が新たに追加された。この関数は現在catchされていない例外の個数を返す。

struct X
{
    ~X()
    {
        try {
            if ( int x = std::uncaught_exceptions() ; x > 1 )
            {
                // ネストされた例外
            }
        } catch( ... )
    }

} ;

apply : tupleの要素を実引数に関数を呼び出す

template <class F, class Tuple>
constexpr decltype(auto) apply(F&& f, Tuple&& t);

std::applytupleのそれぞれの要素を順番に実引数に渡して関数を呼び出すヘルパー関数だ。

ある要素数Ntuple tと関数オブジェクトfに対して、apply( f, t )は、f( get<0>(t), get<1>(t), ... , get<N-1>(t) )のようにfを関数呼び出しする。

template < typename ... Types >
void f( Types ... args ) { }

int main()
{
    // int, int, int
    std::tuple t1( 1,2,3 ) ;

    // f( 1, 2, 3 )の関数呼び出し
    std::apply( f, t1 ) ;

    // int, double, const char *
    std::tuple t2( 123, 4.56, "hello" ) ;

    // f( 123, 4.56, "hello" )の関数呼び出し
    std::apply( f, t2 ) ;
}

Searcher : 検索

C++17では<functional>searcherというライブラリが追加された。これは順序のあるオブジェクトの集合に、ある部分集合(パターン)が含まれているかどうかを検索するためのライブラリだ。その最も一般的な応用例は文字列検索となる。

searcherの基本的な設計としては、クラスのオブジェクトを構築して、コンストラクターで検索したい部分集合(パターン)を与え、operator ()で部分集合が含まれているかを検索したい集合を与える。

この設計のライブラリが追加された理由は、パターンの検索のために何らかの事前の準備を状態として保持しておきたい検索アルゴリズムを実装するためだ。

default_searcher

クラスstd::default_searcherは以下のように宣言されている。

template <  class ForwardIterator1,
            class BinaryPredicate = equal_to<> >
class default_searcher {
public:
    // コンストラクター
    default_searcher( 
        ForwardIterator1 pat_first, ForwardIterator1 pat_last
        , BinaryPredicate pred = BinaryPredicate() ) ;

    // operator ()
    template <class ForwardIterator2>
    pair<ForwardIterator2, ForwardIterator2>
    operator()(ForwardIterator2 first, ForwardIterator2 last) const ;
} ;

コンストラクターで部分集合を受け取る。operator ()で集合を受け取り、部分集合(パターン)と一致した場所をイテレーターのペアで返す。見つからない場合、イテレーターのペアは[last, last)になっている。

以下のように使う。

int main()
{
    std::string pattern("fox") ;
    std::default_searcher
        fox_searcher( std::begin(pattern), std::end(pattern) ) ;

    std::string corpus = "The quick brown fox jumps over the lazy dog" ;

    auto[first, last] = fox_searcher( std::begin(corpus),
                                      std::end(corpus) ) ;
    std::string fox( first, last ) ;
}

default_searcherの検索は、内部的にstd::searchが使われる。

boyer_moore_searcher

std::boyer_moore_searcherはBoyer-Moore文字列検索アルゴリズムを使って部分集合の検索を行う。

Boyer-Moore文字列検索アルゴリズムは極めて効率的な文字列検索のアルゴリズムだ。Boyer-MooreアルゴリズムはBob BoyerとStrother Mooreによって発明され、1977年のCommunications of the ACMで発表された。その内容は以下のURLで読むことができる。

http://www.cs.utexas.edu/~moore/publications/fstrpos.pdf

愚直に実装した文字列検索アルゴリズムは検索すべき部分文字列(パターン)を検索対象の文字列(コーパス)から探す際、パターンの先頭の文字をコーパスの先頭から順に探していき、見つかれば2文字目以降も一致するかどうかを調べる。

Boyer-Mooreアルゴリズムはパターンの末尾の文字から調べる。文字が一致しなければ、パターンから絶対に不一致であるとわかっている長さだけの文字を比較せずに読み飛ばす。これによって効率的な文字列検索を実現している。

Boyer-Mooreアルゴリズムは事前にパターンのどの文字が不一致ならば何文字比較せずに読み飛ばせるかという情報を計算した2つのテーブルを生成する必要がある。このため、Boyer-Mooreアルゴリズムはメモリー使用量と検索前の準備時間というコストがかかる。そのコストは、より効率的な検索により相殺できる。特に、パターンが長い場合は効果的だ。

C++17に入るBoyer-Mooreアルゴリズムに基づく検索は、テンプレートを使った汎用的なchar型のような状態数の少ない型に対しての実装だけではなく、ハッシュを使ったハッシュマップのようなデータ構造を使うことにより、任意の型に対応できる汎用的な設計になっている。

クラスboyer_moore_searcherは以下のように宣言されている。

template <
    class RandomAccessIterator1,
    class Hash = hash<
        typename iterator_traits<RandomAccessIterator1>::value_type>,
    class BinaryPredicate = equal_to<> >
class boyer_moore_searcher {
public:
    // コンストラクター
    boyer_moore_searcher(
        RandomAccessIterator1 pat_first,
        RandomAccessIterator1 pat_last,
        Hash hf = Hash(),
        BinaryPredicate pred = BinaryPredicate() ) ;

    // operator ()
    template <class RandomAccessIterator2>
    pair<RandomAccessIterator2, RandomAccessIterator2>
    operator()( RandomAccessIterator2 first,
                RandomAccessIterator2 last) const;
} ;

boyer_moore_searcherは、文字列以外にも適用できる汎用的な設計のため、ハッシュ関数を取る。char型のような取りうる状態数の少ない型以外が渡された場合は、std::unordered_mapのようなメモリー使用量を削減できる何らかのデータ構造を使ってテーブルを構築する。

使い方はdefault_searcherとほとんど変わらない。

int main()
{
    std::string pattern("fox") ;
    std::boyer_moore_searcher
        fox_searcher( std::begin(pattern), std::end(pattern) ) ;

    std::string corpus = "The quick brown fox jumps over the lazy dog" ;

    auto[first, last] = fox_searcher( std::begin(corpus),
        std::end(corpus) ) ;
    std::string fox( first, last ) ;
}

boyer_moore_horspool_searcher

std::boyer_moore_horspool_searcherはBoyer-Moore-Horspool検索アルゴリズムを使って部分集合の検索を行う。Boyer-Moore-HorspoolアルゴリズムはNigel Horspoolによって1980年に発表された。

参考:"Practical fast searching in strings" 1980

Boyer-Moore-Horspoolアルゴリズムは内部テーブルに使うメモリー使用量を削減しているが、最悪計算量の点でオリジナルのBoyer-Mooreアルゴリズムには劣っている。つまり、実行時間の増大を犠牲にしてメモリー使用量を削減したトレードオフなアルゴリズムと言える。

クラスboyer_moore_horspool_searcherの宣言は以下のとおり。

template <
    class RandomAccessIterator1,
    class Hash = hash<
        typename iterator_traits<RandomAccessIterator1>::value_type>,
    class BinaryPredicate = equal_to<> >
class boyer_moore_horspool_searcher {
public:
    // コンストラクター
    boyer_moore_horspool_searcher(
        RandomAccessIterator1 pat_first,
        RandomAccessIterator1 pat_last,
        Hash hf = Hash(),
        BinaryPredicate pred = BinaryPredicate() );

    // operator () 
    template <class RandomAccessIterator2>
    pair<RandomAccessIterator2, RandomAccessIterator2>
    operator()( RandomAccessIterator2 first,
                RandomAccessIterator2 last) const;
} ;

使い方はboyer_moore_searcherと変わらない。

int main()
{
    std::string pattern("fox") ;
    std::boyer_moore_horspool_searcher
        fox_searcher( std::begin(pattern), std::end(pattern) ) ;

    std::string corpus = "The quick brown fox jumps over the lazy dog" ;

    auto[first, last] = fox_searcher(   std::begin(corpus),
                                        std::end(corpus) ) ;
    std::string fox( first, last ) ;
}

sample : 乱択アルゴリズム

template <  class PopulationIterator, class SampleIterator,
            class Distance, class UniformRandomBitGenerator >

SampleIterator sample(
    PopulationIterator first, PopulationIterator last,
    SampleIterator out,
    Distance n, UniformRandomBitGenerator&& g) ;

C++17で<algorithm>に追加されたstd::sampleは、標本を確率的に選択するための乱択アルゴリズムだ。

[first, last)は標本を選択する先の集合を指すイテレーター。outは標本を出力する先のイテレーター。nは選択する標本の個数。gは標本を選択するのに使う乱数生成器。戻り値はout

ある要素の集合から、n個の要素を確率的に公平に選択したい場合に使うことができる。

乱択アルゴリズム

std::sampleを使う前に、まず正しい乱択アルゴリズムについて学ぶ必要がある。乱択アルゴリズムについて詳しくは、Donald E. Knuthの The Art of Computer Programming (以下TAOCP、邦訳はアスキードワンゴから同名の書名で出版されている)を参照。

ユーザーからの入力、計測した気象情報のデータ、サーバーへのアクセスログなど、世の中には膨大な量のデータが存在する。これらの膨大なデータをすべて処理するのではなく、標本を採取することによって、統計的にそれなりに信頼できる確率で正しい全体のデータを推定することができる。そのためには\(n\)個の標本をバイアスがかからない方法で選択する必要がある。バイアスのかからない方法で\(n\)個の標本を取り出すには、集合の先頭から\(n\)個とか、1つおきに\(n\)個といった方法で選択してはならない。それはバイアスがかかっている。

ある値の集合から、バイアスのかかっていない\(n\)個の標本を得るには、集合のすべての値が等しい確率で選ばれた上で\(n\)個を選択しなければならない。いったいどうすればいいのだろうか。

std::sampleを使えば、100個の値から10個の標本を得るのは、以下のように書くことが可能だ。

int main()
{
    // 100個の値の集合
    std::vector<int> pop(100) ;
    std::iota( std::begin(pop), std::end(pop), 0 ) ;

    // 標本を格納するコンテナー
    std::vector<int> out(10) ;

    // 乱数生成器
    std::array<std::uint32_t, sizeof(std::knuth_b)/4> a ;
    std::random_device r ;
    std::generate( std::begin(a), std::end(a), [&]{ return r() ; } ) ;
    std::seed_seq seed( std::begin(a), std::end(a) ) ;
    std::knuth_b g( seed ) ;

    // 10個の標本を得る
    sample( std::begin(pop), std::end(pop), std::begin(out), 10, g ) ;

    // 標本を出力
    std::copy(  std::begin(out), std::end(out),
                std::ostream_iterator<int>(std::cout, ", ") ) ;
}

集合に含まれる値の数が\(N\)個だとわかっているならば、それぞれの値について\(n/m\)の確率で選ぶというのはどうだろうか。100個中10個を選ぶのであれば、\(1/10\)の確率でそれぞれの値を標本として選択することになる。

この考えに基づく乱択アルゴリズムは以下のようになる。

  1. 集合の要素数を\(N\)、選択すべき標本の数を\(n\), \(i\)を0とする。
  2. 0ベースインデックスで\(i\)番目の値を\(n/m\)の確率で標本として選択する。
  3. \(i\)をインクリメントする。
  4. \(i\ != N\)ならばgoto 2。

このアルゴリズムをコードで書くと以下のようになる。

template <  class PopulationIterator, class SampleIterator,
            class Distance, class UniformRandomBitGenerator >
SampleIterator sample(
    PopulationIterator first, PopulationIterator last,
    SampleIterator out,
    Distance n, UniformRandomBitGenerator&& g)
{
    auto N = std::distance( first, last ) ;

    // 確率n/Nでtrueを返すベルヌーイ分布
    double probability = double(n)/double(N) ;
    std::bernoulli_distribution d( probability ) ;

    // それぞれの値に対して
    std::for_each( first, last,
        [&]( auto && value )
        {
            if ( d(g) )
            {// n/Nの確率で標本として選択する
                *out = value ;
                ++out ;
            }
        } ) ;

    return out ;
}

残念ながらこのアルゴリズムは正しく動かない。この例では、100個の値の集合から10個の標本を選択したい。しかし、選ばれる標本の数はプログラムの実行ごとに異なる。このアルゴリズムは、標本の数が平均的に10個選ばれることが期待できるが、運が悪いと0個や100個の標本が選ばれてしまう可能性がある。

ちなみに、TAOCP Vol. 2によれば、このとき選ばれる標本数の標準偏差は\(\sqrt{n(1-n/N)}\)になる。

正しいアルゴリズムは、要素の集合のうちの\((t+1)\)番目の要素は、すでに\(m\)個の要素が標本として選ばれたとき、\((n-m)(N-t)\)の確率で選ぶものだ。

アルゴリズムS:選択標本、要素数がわかっている集合からの標本の選択

KnuthのTAOCP Vol. 2では、アルゴリズムSと称して、要素数のわかっている集合から標本を選択する方法を解説している。

アルゴリズムSは以下のとおり。

\(0 < n \leq N\) のとき、\(N\)個の集合から\(n\)個の標本をランダムに選択する。

  1. \(t\), \(m\)を0とする。\(t\)はこれまでに処理した要素数、\(m\)は標本として選択した要素数とする。
  2. \(0 \leq U \leq N - t\)の範囲の乱数\(U\)を生成する。
  3. \(U \geq n - m\)であればgoto 5。
  4. 次の要素を標本として選択する。\(m\)\(t\)をインクリメントする。\(m < n\)であれば、goto 2。そうでなければ標本は完了したのでアルゴリズムは終了する。
  5. 次の要素を標本として選択しない。\(t\)をインクリメントする。goto 2。

実装は以下のようになる。

template <  class PopulationIterator, class SampleIterator,
            class Distance, class UniformRandomBitGenerator >
SampleIterator
sample_s(
    PopulationIterator first, PopulationIterator last,
    SampleIterator out,
    Distance n, UniformRandomBitGenerator&& g)
{
    // 1.
    Distance t = 0 ;
    Distance m = 0 ;
    const auto N = std::distance( first, last ) ;

    auto r = [&]{
        std::uniform_int_distribution<> d(0, N-t) ;
        return d(g) ;
    } ;

    while ( m < n  && first != last )
    {
        // 2. 3.
        if ( r() >= n - m )
        {// 5.
            ++t ;
            ++first ;
        }
        else { // 4.
            *out = *first ;
            ++first ; ++out ;
            ++m ; ++t ;
        }
    }
    
    return out ;
}

アルゴリズムR:保管標本、要素数がわからない集合からの標本の選択

アルゴリズムSは集合の要素数が\(N\)個であるとわかっている場合に、\(n\)個の標本を選択するアルゴリズムだ。では、もし\(N\)がわからない場合はどうすればいいのだろうか。

現実には\(N\)がわからない状況がよくある。

このような要素数のわからない入力にアルゴリズムSを適用するには、まず一度全部入力を得て、全体の要素数を確定させた上で、全要素に対してアルゴリズムSを適用させるという2段階の方法を使うことができる。

しかし、1段階の要素の巡回だけで済ませたい。要素数のわからない入力を処理して、その時点で公平に選択された標本を得たい。

アルゴリズムRはそのような状況で使えるアルゴリズムだ。

アルゴリズムRでは、要素数のわからない要素の集合から\(n\)個の標本を選択する。そのために標本として選択した要素を保管しておき、新しい入力が与えられるたびに、標本として選択するかどうかの判断をし、選択をするのであれば、保管しておいた既存の標本と置き換える。

アルゴリズムRは以下のとおり(このアルゴリズムはKnuth本とは違う)。

\(n > 0\)のとき、\(size \geq n\)である未確定の\(size\)個の要素数を持つ入力から、\(n\)個の標本をランダムに選択する。標本の候補は\(n\)個まで保管される。\(1 \leq j \leq n\)のとき\(I[j]\)は保管された標本を指す。

  1. 入力から最初の\(n\)個を標本として選択し、保管する。\(1 \leq j \leq n\)の範囲で\(I[j]\)\(j\)番目の標本を保管する。\(t\)の値を\(n\)とする。\(I[1]\), ..., \(I[n]\)は現在の標本を指す。\(t\)は現在処理した入力の個数を指す。
  2. 入力の終わりであればアルゴリズムを終了する。
  3. \(t\)をインクリメントする。\(1 \leq M \leq t\)の範囲の乱数\(M\)を生成する。\(M > n\) ならばgoto 5。
  4. 次の入力を\(I[M]\)に保管する。goto 2。
  5. 次の入力を保管しない。goto 2。

実装は以下のようになる。

template <  class PopulationIterator, class SampleIterator,
            class Distance, class UniformRandomBitGenerator >

SampleIterator sample_r(
    PopulationIterator first, PopulationIterator last,
    SampleIterator out,
    Distance n, UniformRandomBitGenerator&& g)
{
    Distance t = 0 ;

    auto result = out ;

    for ( ; (first != last) && (t != n) ; ++first, ++t, ++result )
    {
        out[t] = *first ;
    }

    if ( t != n )
        return result ;


    auto I = [&](Distance j) -> decltype(auto) { return out[j-1] ; } ;

    while ( first != last )
    {
        ++t ;
        std::uniform_int_distribution<Distance> d( 1, t ) ;
        auto M = d(g) ;

        if ( M > n )
        {
            ++first ;
        }
        else {
            I(M) = *first ;
            ++first ;
        }
    }

    return result ;
}

C++のsample

ここまで説明したように、乱択アルゴリズムには2種類ある。入力の要素数がわかっている場合のアルゴリズムS(選択標本)と、入力の要素数がわからない場合のアルゴリズムR(保管標本)だ。

しかし、C++に追加された乱択アルゴリズムの関数テンプレートの宣言は、はじめに説明したように以下の1つしかない。並列アルゴリズムには対応していない。

template<
    class PopulationIterator, class SampleIterator,
    class Distance, class UniformRandomBitGenerator >
SampleIterator
sample(
    PopulationIterator first, PopulationIterator last,
    SampleIterator out,
    Distance n, UniformRandomBitGenerator&& g) ;

[first, last)は標本を選択する先の集合を指すイテレーター。outは標本を出力する先のイテレーター。nは選択する標本の個数。gは標本を選択するのに使う乱数生成器。戻り値はout

samplePopulationIteratorSampleIteratorのイテレーターカテゴリーによって、どちらのアルゴリズムを使うべきか判断している。

アルゴリズムS(選択標本)を使う場合、PopulationIteratorは前方イテレーター、SampleIteratorは出力イテレーターを満たさなければならない。

アルゴリズムR(保管標本)を使う場合、PopulationIteratorは入力イテレーター、SampleIteratorはランダムアクセスイテレーターを満たさなければならない。

これはどういうことかというと、要素数の取得のためには、入力元のPopulationIterator [first, last)から要素数を得る必要があり、そのためにはPopulationIteratorは前方イテレーターを満たしていなければならない。その場合、選択した標本はそのままイテレーターに出力すればいいので、出力先のSampleIteratorは出力イテレーターを満たすだけでよい。

もし入力元のPopulationIteratorが入力イテレーターしか満たさない場合、この場合はPopulationIterator[first, last)から要素数を得ることができないので、要素数がわからないときに使えるアルゴリズムR(保管標本)を選択せざるを得ない。その場合、入力を処理するに連れて、新たに選択した標本が既存の標本を上書きするので、出力先のSampleIteratorはランダムアクセスイテレーターである必要がある。

int main()
{
    std::vector<int> input ;

    std::knuth_b g ;

    // PopulationIteratorは前方イテレーターを満たす
    // SampleIteratorは出力イテレーターでよい
    std::sample(    std::begin(input), std::end(input),
                    std::ostream_iterator<int>(std::cout), 100
                    g ) ;

    std::vector<int> sample(100) ;

    // PopulationIteratorは入力イテレーターしか満たさない
    // SampleIteratorにはランダムアクセスイテレーターが必要
    std::sample(
        std::istream_iterator<int>(std::cin),
        std::istream_iterator<int>{},
        std::begin(sample), 100, g ) ;

}

注意が必要なこととして、C++のsampleは入力元のPopulationIteratorが前方イテレーター以上を満たす場合は、必ずアルゴリズムS(選択標本)を使うということだ。これはつまり、要素数を得るためにstd::distance(first, last)が行われるということを意味する。もしこの処理が非効率的なイテレーターを渡した場合、必要以上に非効率的なコードになってしまう。

たとえば以下のコードは、

int main()
{
    std::list<int> input(10000) ;
    std::list<int> sample(100) ;
    std::knuth_b g ;

    std::sample(    std::begin(input), std::end(input),
                    std::begin(sample), 100, g ) ;
}

以下のような意味を持つ。

int main()
{
    std::list<int> input(10000) ;
    std::list<int> sample(100) ;
    std::knuth_b g ;

    std::size_t count = 0 ;
    
    // 要素数の得るためにイテレーターを回す
    // 非効率的
    for( auto && e : input )
    { ++count ; }

    // 標本の選択のためにイテレーターを回す
    for ( auto && e : input )
    {/* 標本の選択 */}
}

std::listのメンバー関数sizeは定数時間であることが保証されているため、このコードにおけるイテレーターを回すループは1回に抑えられる。しかし、std::sampleは要素数を渡す実引数がないために要素数がイテレーターを全走査しなくてもわかっている場合でも、非効率的な処理を行わなければならない。

もしランダムアクセスイテレーター未満、前方イテレーター以上のイテレーターカテゴリーのイテレーターの範囲から標本を選択したい場合で、イテレーターの範囲の指す要素数があらかじめわかっている場合は、自前でアルゴリズムSを実装したほうが効率がよい。

template <  class PopulationIterator, class SampleIterator,
            class Distance, class UniformRandomBitGenerator >
SampleIterator
sample_s(
    PopulationIterator first, PopulationIterator last,
    Distance size,
    SampleIterator out,
    Distance n, UniformRandomBitGenerator&& g)
{
    // 1.
    Distance t = 0 ;
    Distance m = 0 ;
    const auto N = size ;

    auto r = [&]{
        std::uniform_int_distribution<> d(0, N-t) ;
        return d(g) ;
    } ;

    while ( m < n  && first != last )
    {
        // 2. 3.
        if ( r() >= n - m )
        {// 5.
            ++t ;
            ++first ;
        }
        else { // 4.
            *out = *first ;
            ++first ; ++out ;
            ++m ; ++t ;
        }
    }
    
    return out ;
}

shared_ptr<T[]> : 配列に対するshared_ptr

C++17では、shared_ptrが配列に対応した。

int main()
{
    // 配列対応のshared_ptr
    std::shared_ptr< int [] > ptr( new int[5] ) ;

    // operator []で配列に添字アクセスできる
    ptr[0] = 42 ;

    // shared_ptrのデストラクターがdelete[]を呼び出す
}

as_const: const性の付与

as_constはヘッダーファイル<utility>で定義されている。

template <class T> constexpr add_const_t<T>& as_const(T& t) noexcept
{
    return t ;
}

as_constは引数として渡したlvalueリファレンスをconstlvalueリファレンスにキャストする関数だ。const性を付与する手軽なヘルパー関数として使うことができる。

// 1
template < typename T >
void f( T & ) {}
// 2、こちらを呼び出したい
template < typename T >
void f( T const & ) { }

int main()
{
    int x{} ;

    f(x) ; // 1

    // constを付与する冗長な方法
    int const & ref = x ;
    f(ref) ; // 2

    // 簡潔
    f( std::as_const(x) ) ; // 2
}

make_from_tuple : tupleの要素を実引数にコンストラクターを呼び出す

make_from_tupleはヘッダーファイル<tuple>で定義されている。

template <class T, class Tuple>
constexpr T make_from_tuple(Tuple&& t);

applytupleの要素を実引数に関数を呼び出すライブラリだが、make_from_tupletupleの要素を実引数にコンストラクターを呼び出すライブラリだ。

ある型Tと要素数Ntuple tに対して、make_from_tuple<T>(t)は、T型をT( get<0>(t), get<1>(t), ... , get<N-1>(t) )のように構築して、構築したT型のオブジェクトを返す。

class X
{
    template < typename ... Types >
    T( Types ... ) { }
} ;

int main()
{
    // int, int, int
    std::tuple t1(1,2,3) ;

    // X(1,2,3)
    X x1 = std::make_from_tuple<X>( t1 ) 

    // int, double, const char *
    std::tuple t2( 123, 4.56, "hello" ) ;

    // X(123, 4.56, "hello")
    X x2 = std::make_from_tuple<X>( t2 ) ;
}

invoke : 指定した関数を指定した実引数で呼び出す

invokeはヘッダーファイル<functional>で定義されている。

template <class F, class... Args>
invoke_result_t<F, Args...> invoke(F&& f, Args&&... args)
noexcept(is_nothrow_invocable_v<F, Args...>);

invoke( f, t1, t2, ... , tN )は、関数ff( a1, a2, ... , aN )のように呼び出す。

より正確には、C++標準規格のINVOKE(f, t1, t2, ... , tN)と同じ規則で呼び出す。これにはさまざまな規則があり、たとえばメンバー関数へのポインターやデータメンバーへのポインター、またその場合に与えるクラスへのオブジェクトがリファレンスかポインターかreference_wrapperかによっても異なる。その詳細はここでは解説しない。

INVOKEstd::functionstd::bindでも使われている規則なので、標準ライブラリと同じ挙動ができるようになると覚えておけばよい。

void f( int ) { }

struct S
{
    void f( int ) ;
    int data ;
} ;

int main()
{
    // f( 1 ) 
    std::invoke( f, 1 ) ;

    S s ;

    // (s.*&S::f)(1)
    std::invoke( &S::f, s, 1 ) ;
    // ((*&s).*&S::f)(1)
    std::invoke( &S::f, &s, 1 ) ;
    // s.*&S::data 
    std::invoke( &S::data, s ) ;
}

not_fn : 戻り値の否定ラッパー

not_fnはヘッダーファイル<functional>で定義されている。

template <class F> unspecified not_fn(F&& f);

関数オブジェクトfに対してnot_fn(f)を呼び出すと、戻り値として何らかの関数オブジェクトが返ってくる。その関数オブジェクトを呼び出すと、実引数をfに渡してfを関数呼び出しして、戻り値をoperator !で否定して返す。

int main()
{

    auto r1 = std::not_fn( []{ return true ; } ) ;

    r1() ; // false

    auto r2 = std::not_fn( []( bool b ) { return b ; } ) ;

    r2(true) ; // false
}

すでに廃止予定になったnot1, not2の代替品。

メモリー管理アルゴリズム

C++17ではヘッダーファイル<memory>にメモリー管理用のアルゴリズムが追加された。

addressof

template <class T> constexpr T* addressof(T& r) noexcept;

addressofはC++17以前からもある。addressof(r)rのポインターを取得する。たとえ、rの型がoperator &をオーバーロードしていても正しいポインターを取得できる。

struct S
{
    S * operator &() const noexcept
    { return nullptr ; } 
} ;

int main()
{
    S s ;

    // nullptr
    S * p1 = & s ;
    // 妥当なポインター
    S * p2 = std::addressof(s) ;

}

uninitialized_default_construct

template <class ForwardIterator>
void uninitialized_default_construct(
    ForwardIterator first, ForwardIterator last);

template <class ForwardIterator, class Size>
ForwardIterator uninitialized_default_construct_n(
    ForwardIterator first, Size n);

[first, last)の範囲、もしくはfirstからn個の範囲の未初期化のメモリーをtypename iterator_traits<ForwardIterator>::value_typeでデフォルト初期化する。2つ目のアルゴリズムはfirstからn個をデフォルト初期化する。

int main()
{
    std::shared_ptr<void> raw_ptr
    (   ::operator new( sizeof(std::string) * 10 ),
        [](void * ptr){ ::operator delete(ptr) ; } ) ;
 
    std::string * ptr = static_cast<std::string *>( raw_ptr.get() ) ;

    std::uninitialized_default_construct_n( ptr, 10 ) ;
    std::destroy_n( ptr, 10 ) ;
}

uninitialized_value_construct

template <class ForwardIterator>
void uninitialized_value_construct(
    ForwardIterator first, ForwardIterator last);

template <class ForwardIterator, class Size>
ForwardIterator uninitialized_value_construct_n(
    ForwardIterator first, Size n);

使い方はuninitialized_default_constructと同じ。ただし、こちらはデフォルト初期化ではなく値初期化する。

uninitialized_copy

template <class InputIterator, class ForwardIterator>
ForwardIterator
uninitialized_copy( InputIterator first, InputIterator last,
                    ForwardIterator result);

template <class InputIterator, class Size, class ForwardIterator>
ForwardIterator
uninitialized_copy_n(   InputIterator first, Size n,
                        ForwardIterator result);

[first, last)の範囲、もしくはfirstからn個の範囲の値を、resultの指す未初期化のメモリーにコピー構築する。

int main()
{
    std::vector<std::string> input(10, "hello") ;

    std::shared_ptr<void> raw_ptr
    (   ::operator new( sizeof(std::string) * 10 ),
        [](void * ptr){ ::operator delete(ptr) ; } ) ;
 
    std::string * ptr = static_cast<std::string *>( raw_ptr.get() ) ;


    std::uninitialized_copy_n( std::begin(input), 10, ptr ) ;
    std::destroy_n( ptr, 10 ) ;
}

uninitialized_move

template <class InputIterator, class ForwardIterator>
ForwardIterator
uninitialized_move( InputIterator first, InputIterator last,
                    ForwardIterator result);

template <class InputIterator, class Size, class ForwardIterator>
pair<InputIterator, ForwardIterator>
uninitialized_move_n(   InputIterator first, Size n,
                        ForwardIterator result);

使い方はuninitialized_copyと同じ。ただしこちらはコピーではなくムーブする。

uninitialized_fill

template <class ForwardIterator, class T>
void uninitialized_fill(
    ForwardIterator first, ForwardIterator last,
    const T& x);

template <class ForwardIterator, class Size, class T>
ForwardIterator uninitialized_fill_n(
    ForwardIterator first, Size n,
    const T& x);

[first, last)の範囲、もしくはfirstからn個の範囲の未初期化のメモリーを、コンストラクターに実引数xを与えて構築する。

destroy

template <class T>
void destroy_at(T* location);

location->~T()を呼び出す。

template <class ForwardIterator>
void destroy(ForwardIterator first, ForwardIterator last);

template <class ForwardIterator, class Size>
ForwardIterator destroy_n(ForwardIterator first, Size n);

[first, last)の範囲、もしくはfirstからn個の範囲にdestroy_atを呼び出す。

shared_ptr::weak_type

C++17ではshared_ptrweak_typeというネストされた型名が追加された。これはshared_ptrに対するweak_ptrtypedef名となっている。

namespace std {

template < typename T >
class shared_ptr
{
    using weak_type = weak_ptr<T> ;
} ;

}

使い方

template < typename Shared_ptr >
void f( Shared_ptr sptr )
{
    // C++14
    auto wptr1 = std::weak_ptr<
                    typename Shared_ptr::element_type
                >( sptr ) ;

    // C++17
    auto wptr2 = typename Shared_ptr::weak_type( sptr ) ;
}

void_t

ヘッダーファイル<type_traits>で定義されているvoid_tは以下のように定義されている。

namespace std {

template < class ... >
using void_t = void ;

}

void_tは任意個の型をテンプレート実引数として受け取るvoid型だ。この性質はテンプレートメタプログラミングにおいてとても便利なので、標準ライブラリに追加された。

bool_constant

ヘッダーファイル<type_traits>bool_constantが追加された。

template <bool B>
using bool_constant = integral_constant<bool, B>;

using true_type = bool_constant<true>;
using false_type = bool_constant<false>;

今までintegral_constantを使っていた場面で特にboolだけが必要な場面では、C++17以降は単にstd::true_typestd::false_typeと書くだけでよくなる。

type_traits

C++17では<type_traits>に機能追加が行われた。

変数テンプレート版traits

C++17では、既存のtraitsに変数テンプレートを利用した_v版が追加された。

たとえば、is_integral<T>::valueと書く代わりにis_integral_v<T>と書くことができる。

template < typename T >
void f( T x )
{
    constexpr bool b1 = std::is_integral<T>::value ; // データメンバー
    constexpr bool b2 = std::is_integral_v<T> ; // 変数テンプレート
    constexpr bool b3 = std::is_integral<T>{} ; // operator bool()
}

論理演算traits

C++17ではクラステンプレートconjunction, disjunction, negationが追加された。これはテンプレートメタプログラミングで論理積、論理和、否定を手軽に扱うためのtraitsだ。

conjunction : 論理積

template<class... B> struct conjunction;

クラステンプレートconjunction<B1, B2, ..., BN>はテンプレート実引数B1, B2, ..., BNに論理積を適用する。conjunctionはそれぞれのテンプレート実引数Biに対して、bool(Bi::value)falseとなる最初の型を基本クラスに持つか、あるいは最後のBNを基本クラスに持つ。

int main()
{
    using namespace std ;

    // is_void<void>を基本クラスに持つ
    using t1 =
        conjunction<
            is_same<int, int>, is_integral<int>,
            is_void<void> > ;

    // is_integral<double>を基本クラスに持つ
    using t2 =
        conjunction<
            is_same<int, int>, is_integral<double>,
            is_void<void> > ;

}

disjunction : 論理和

template<class... B> struct disjunction;

クラステンプレートdisjunction<B1, B2, ..., BN>はテンプレート実引数B1, B2, ..., BNに論理和を適用する。disjunctionはそれぞれのテンプレート実引数Biに対して、bool(Bi::value)trueとなる最初の型を基本クラスに持つか、あるいは最後のBNを基本クラスに持つ。

int main()
{
    using namespace std ;

    // is_same<int,int>を基本クラスに持つ
    using t1 =
        disjunction<
            is_same<int, int>, is_integral<int>,
            is_void<void> > ;

    // is_void<int>を基本クラスに持つ
    using t2 =
        disjunction<
            is_same<int, double>, is_integral<double>,
            is_void<int> > ;
}

negation : 否定

template<class B> struct negation;

クラステンプレートnegation<B>Bに否定を適用する。negationは基本クラスとしてbool_constant<!bool(B::value)>を持つ。

int main()
{
    using namespace std ;

    // false
    constexpr bool b1 = negation< true_type >::value ;
    // true
    constexpr bool b2 = negation< false_type >::value ; 
}

is_invocable : 呼び出し可能か確認するtraits

template <class Fn, class... ArgTypes>
struct is_invocable;

template <class R, class Fn, class... ArgTypes>
struct is_invocable_r;

template <class Fn, class... ArgTypes>
struct is_nothrow_invocable;

template <class R, class Fn, class... ArgTypes>
struct is_nothrow_invocable_r;

is_invocableはテンプレート実引数で与えられた型FnがパラメーターパックArgTypesをパック展開した結果を実引数に関数呼び出しできるかどうか、そしてその戻り値はRへ暗黙変換できるかどうかを確認するtraitsだ。呼び出せるのであればtrue_type, そうでなければfalse_typeを基本クラスに持つ。

is_invocableは関数呼び出しした結果の戻り値の型については問わない。

is_invocable_rは呼び出し可能性に加えて、関数呼び出しした結果の戻り値の型がRへ暗黙変換できることが確認される。

is_nothrow_invocableis_nothrow_invocable_rは、関数呼び出し(および戻り値型Rへの暗黙変換)が無例外保証されていることも確認する。

int f( int, double ) ;

int main()
{
    // true
    constexpr bool b1 =
        std::is_invocable< decltype(&f), int, double >{} ;
    // true
    constexpr bool b2 =
        std::is_invocable< decltype(&f), int, int >{} ;

    // false
    constexpr bool b3 =
        std::is_invocable< decltype(&f), int >{} ;
    // false
    constexpr bool b4 =
        std::is_invocable< decltype(&f), int, std::string >{} ;
    
    // true
    constexpr bool b5 = 
        std::is_invocable_r< int, decltype(&f), int, double >{} ;
    // false
    constexpr bool b6 =
        std::is_invocable_r< double, decltype(&f), int, double >{} ;
}

has_unique_object_representations : 同値の内部表現が同一か確認するtraits

template <class T>
struct has_unique_object_representations ;

has_unique_object_representations<T>は、T型がトリビアルにコピー可能で、かつT型の同値である2つのオブジェクトの内部表現が同じ場合に、trueを返す。

falseを返す例としては、オブジェクトがパディング(padding)と呼ばれるアライメント調整などのための値の表現に影響しないストレージ領域を持つ場合だ。パディングビットの値は同値に影響しないので、falseを返す。

たとえば以下のようなクラスXは、

struct X
{
    std::uint8_t a ;
    std::uint32_t b ;
} ;

ある実装においては、4バイトにアライメントする必要があり、そのオブジェクトの本当のレイアウトは以下のようになっているかもしれない。

struct X
{
    std::uint8_t a ;

    std::byte unused_padding[3] ;

    std::uint32_t b ;
} ;

この場合、unused_paddingの値には意味がなく、クラスXの同値比較には用いられない。この場合、std::has_unique_representations_v<X>falseになる。

is_nothrow_swappable : 無例外swap可能か確認するtraits

template <class T>
struct is_nothrow_swappable;

template <class T, class U>
struct is_nothrow_swappable_with;

is_nothrow_swappable<T>T型がswapで例外を投げないときにtrueを返す。

is_nothrow_swappable_with<T, U>は、T型とU型を相互にswapするときに例外を投げないときにtrueを返す。

コンテナーで不完全型のサポート

注意:この説明は上級者向けだ。

C++17では以下のコードが合法になった。このコードの挙動はC++14までは実装依存であった。

struct X
{
    std::vector<X> v ;
    std::list<X> l ;
    std::forward_list<X> f ;
} ;

クラスはクラス定義の終了である}を持って完全型となる。クラススコープに注入されたクラス名は、クラス定義の中ではまだ完全型ではない。不完全型をコンテナーの要素型に指定した場合の挙動は、C++14までは規定されていなかった。

C++17では、vector, list, forward_listに限り、要素型に一時的に不完全型を許すようになった。実際にコンテナーを使う際には完全型になっていなければならない。

emplaceの戻り値

C++17ではシーケンスコンテナーのemplace_front/emplace_back, queuestackemplaceが構築した要素へのリファレンスを返すように変更された。

そのため、C++14では以下のように書いていたコードが、

int main()
{
    std::vector<int> v ;

    v.emplace_back(0) ; // void
    int value = v.back() ;
}

以下のように書けるようになった。

int main()
{
    std::vector<int> v ;

    int value = v.emplace_back(0) ;
}

mapとunordered_mapの変更

mapunordered_mapに、try_emplaceinsert_or_assignという2つのメンバー関数が入った。このメンバー関数はmulti_mapunordered_multi_mapには追加されていない。

try_emplace

template <class... Args>
pair<iterator, bool>
try_emplace(const key_type& k, Args&&... args);

template <class... Args>
iterator
try_emplace(
    const_iterator hint,
    const key_type& k, Args&&... args);

従来のemplaceは、キーに対応する要素が存在しない場合、要素がargsからemplace構築されて追加される。もし、キーに対応する要素が存在する場合、要素は追加されない。要素が追加されないとき、argsがムーブされるかどうかは実装定義である。

int main()
{
    std::map< int, std::unique_ptr<int> > m ;

    // すでに要素が存在する
    m[0] = nullptr ;

    auto ptr = std::make_unique<int>(0) ;
    // emplaceは失敗する
    auto [iter, is_emplaced] = m.emplace( 0, std::move(ptr) ) ;

    // 結果は実装により異なる
    // ptrはムーブされているかもしれない
    bool b = ( ptr != nullptr ) ;
}

この場合、実際にmapに要素は追加されていないのに、ptrはムーブされてしまうかもしれない。

このため、C++17では、要素が追加されなかった場合argsはムーブされないことが保証されるtry_emplaceが追加された。

int main()
{
    std::map< int, std::unique_ptr<int> > m ;

    // すでに要素が存在する
    m[0] = nullptr ;

    auto ptr = std::make_unique<int>(0) ;
    // try_emplaceは失敗する
    auto [iter, is_emplaced] = m.try_emplace( 0, std::move(ptr) ) ;

    // trueであることが保証される
    // ptrはムーブされていない
    bool b = ( ptr != nullptr ) ;
}

insert_or_assign

template <class M>
pair<iterator, bool>
insert_or_assign(const key_type& k, M&& obj);

template <class M>
iterator
insert_or_assign(
    const_iterator hint,
    const key_type& k, M&& obj);

insert_or_assignkeyに連想された要素が存在する場合は要素を代入し、存在しない場合は要素を追加する。operator []との違いは、要素が代入されたか追加されたかが、戻り値のpairboolでわかるということだ。

int main()
{
    std::map< int, int > m ;
    m[0] = 0 ;

    {
        // 代入
        // is_insertedはfalse
        auto [iter, is_inserted] = m.insert_or_assign( 0, 1 ) ;
    }

    {
        // 追加
        // is_insertedはtrue
        auto [iter, is_inserted] = m.insert_or_assign( 1, 1 ) ;
    }
}

連想コンテナーへのsplice操作

C++17では、連想コンテナーと非順序連想コンテナーでsplice操作がサポートされた。

対象のコンテナーはmap, set, multimap, multiset, unordered_map, unordered_set, unordered_multimap, unordered_multisetだ。

splice操作とはlistで提供されている操作で、アロケーター互換のlistのオブジェクトの要素をストレージと所有権ごと別のオブジェクトに移動する機能だ。

int main()
{
    std::list<int> a = {1,2,3} ;
    std::list<int> b = {4,5,6} ;

    a.splice( std::end(a), b, std::begin(b) ) ;

    // aは{1,2,3,4}
    // bは{5,6}

    b.splice( std::end(b), a ) ;

    // aは{}
    // bは{5,6,1,2,3,4}

}

連想コンテナーでは、ノードハンドルという仕組みを用いて、コンテナーのオブジェクトから要素の所有権をコンテナーの外に出す仕組みで、splice操作を行う。

merge

すべての連想コンテナーと非順序連想コンテナーは、メンバー関数mergeを持っている。コンテナーa, bがアロケーター互換のとき、a.merge(b)は、コンテナーbの要素の所有権をすべてコンテナーaに移す。

int main()
{
    std::set<int> a = {1,2,3} ;
    std::set<int> b = {4,5,6} ;

    // bの要素をすべてaに移す
    a.merge(b) ;

    // aは{1,2,3,4,5,6}
    // bは{}
}

もし、キーの重複を許さないコンテナーの場合で、値が重複した場合、重複した要素は移動しない。

int main()
{
    std::set<int> a = {1,2,3} ;
    std::set<int> b = {1,2,3,4,5,6} ;

    a.merge(b) ;

    // aは{1,2,3,4,5,6}
    // bは{1,2,3}

}

mergeによって移動された要素を指すポインターとイテレーターは、要素の移動後も妥当である。ただし、所属するコンテナーのオブジェクトが変わる。

int main()
{
    std::set<int> a = {1,2,3} ;
    std::set<int> b = {4,5,6} ;


    auto iterator = std::begin(b) ;
    auto pointer = &*iterator ;

    a.merge(b) ;

    // iteratorとpointerはまだ妥当
    // ただし要素はaに所属する
}

ノードハンドル

ノードハンドルとは、コンテナーオブジェクトから要素を構築したストレージの所有権を切り離す機能だ。

ノードハンドルの型は、各コンテナーのネストされた型名node_typeとなる。たとえばstd::set<int>のノードハンドル型は、std::set<int>::node_typeとなる。

ノードハンドルは以下のようなメンバーを持っている。

class node_handle
{
public :
    // ネストされた型名
    using value_type = ... ;        // set限定、要素型
    using key_type = ... ;          // map限定、キー型
    using mapped_type = ... ;       // map限定、マップ型
    using allocator_type = ... ;    // アロケーターの型

    // デフォルトコンストラクター
    // ムーブコンストラクター
    // ムーブ代入演算子


    // 値へのアクセス
    value_type & value() const ;   // set限定
    key_type & key() const ;        // map限定
    mapped_type & mapped() const ;  // map限定

    // アロケーターへのアクセス
    allocator_type get_allocator() const ;

    // 空かどうかの判定
    explicit operator bool() const noexcept ;
    bool empty() const noexcept ;

    void swap( node_handle & ) ;
} ;

setのノードハンドルはメンバー関数valueで値を得る。

int main()
{
    std::set<int> c = {1,2,3} ;

    auto n = c.extract(2) ;

    // n.value() == 2
    // cは{1,3}
}

mapのノードハンドルはメンバー関数keymappedでそれぞれの値を得る。

int main()
{
    std::map< int, int > m =
    {
        {1,1}, {2,2}, {3,3}
    } ;

    auto n = m.extract(2) ;

    // n.key() == 2 
    // n.mapped() == 2
    // mは{{1,1},{3,3}}

}

ノードハンドルはノードをコンテナーから切り離し、所有権を得る。そのため、ノードハンドルによって得たノードは、元のコンテナーから独立し、元のコンテナーオブジェクトの破棄の際にも破棄されない。ノードハンドルのオブジェクトの破棄時に破棄される。このため、ノードハンドルはアロケーターのコピーも持つ。

int main()
{
    std::set<int>::node_type n ;

    {
        std::set<int> c = { 1,2,3 } ;
        // 所有権の移動
        n = c.extract( std::begin(c) ) ;
        // cが破棄される
    }

    // OK
    // ノードハンドルによって所有権が移動している
    int x = n.value() ;

    // nが破棄される
}

extract : ノードハンドルの取得

node_type extract( const_iterator position ) ;
node_type extract( const key_type & x ) ;

連想コンテナーと非順序連想コンテナーのメンバー関数extractは、ノードハンドルを取得するためのメンバー関数だ。

メンバー関数extract(position)は、イテレーターのpositionが指す要素を、コンテナーから除去して、その要素を所有するノードハンドルを返す。

int main()
{
    std::set<int> c = {1,2,3} ;

    auto n1 = c.extract( std::begin(c) ) ;

    // cは{2,3}

    auto n2 = c.extract( std::begin(c) ) ;

    // cは{3}

}

メンバー関数extract(x)は、キーxがコンテナーに存在する場合、その要素をコンテナーから除去して、その要素を所有するノードハンドルを返す。存在しない場合、空のノードハンドルを返す。

int main()
{
    std::set<int> c = {1,2,3} ;

    auto n1 = c.extract( 1 ) ;
    // cは{2,3}

    auto n2 = c.extract( 2 ) ;
    // cは{3}

    // キー4は存在しない
    auto n3 = c.extract( 4 ) ;
    // cは{3}
    // n3.empty() == true
}

キーの重複を許すコンテナーの場合、複数あるうちの1つの所有権が解放される。

int main()
{
    std::multiset<int> c = {1,1,1} ;
    auto n = c.extract(1) ;
    // cは{1,1}
}

insert : ノードハンドルから要素の追加

// キーの重複を許さないコンテナーの場合
insert_return_type  insert(node_type&& nh);
// キーの重複を許すmultiコンテナーの場合
iterator  insert(node_type&& nh);

// ヒント付きのinsert
iterator            insert(const_iterator hint, node_type&& nh);

ノードハンドルをコンテナーのメンバー関数insertの実引数に渡すと、ノードハンドルから所有権をコンテナーに移動する。

int main()
{
    std::set<int> a = {1,2,3} ;
    std::set<int> b = {4,5,6} ;

    auto n = a.extract(1) ;

    b.insert( std::move(n) ) ;

    // n.empty() == true
}

ノードハンドルが空の場合、何も起こらない。

int main()
{
    std::set<int> c ;
    std::set<int>::node_type n ;

    // 何も起こらない
    c.insert( std::move(n) ) ;
}

キーの重複を許さないコンテナーに、すでにコンテナーに存在するキーと等しい値を所有するノードハンドルをinsertしようとすると、insertは失敗する。

int main()
{
    std::set<int> c = {1,2,3} ;

    auto n = c.extract(1) ;
    c.insert( 1 ) ;

    // 失敗する
    c.insert( std::move(n) ) ; 
}

第一引数にイテレーターhintを受け取るinsertの挙動は、従来のinsertと同じだ。要素がhintの直前に追加されるのであれば償却定数時間で処理が終わる。

ノードハンドルを実引数に受け取るinsertの戻り値の型は、キーの重複を許すmultiコンテナーの場合iterator。キーの重複を許さないコンテナーの場合、insert_return_typeとなる。

multiコンテナーの場合、戻り値は追加した要素を指すイテレーターとなる。

int main()
{
    std::multiset<int> c { 1,2,3 } ;

    auto n = c.extract( 1 ) ;

    auto iter = c.insert( n ) ;

    // cは{1,2,3}
    // iterは1を指す
}

キーの重複を許さないコンテナーの場合、コンテナーにネストされた型名insert_return_typeが戻り値の型となる。たとえばset<int>の場合、set<int>::insert_return_typeとなる。

insert_return_typeの具体的な名前は規格上規定されていない。insert_return_typeは以下のようなデータメンバーを持つ型となっている。

struct insert_return_type
{
    iterator position ;
    bool inserted ;
    node_type node ;
} ;

positioninsertによってコンテナーに所有権を移動して追加された要素を指すイテレーター、insertedは要素の追加が行われた場合にtrueとなるbool, nodeは要素の追加が失敗したときにノードハンドルの所有権が移動されるノードハンドルとなる。

insertに渡したノードハンドルが空のとき、insertedfalse, positionend(), nodeは空になる。

int main()
{
    std::set<int> c = {1,2,3} ;
    std::set<int>::node_type n ; // 空

    auto [position, inserted, node] = c.insert( std::move(n) ) ;

    // inserted == false
    // position == c.end()
    // node.empty() == true
}

insertが成功したとき、insertedtrue, positionは追加された要素を指し、nodeは空になる。

int main()
{
    std::set<int> c = {1,2,3} ;
    auto n = c.extract(1) ;

    auto [position, inserted, node] = c.insert( std::move(n) ) ;

    // inserted == true
    // position == c.find(1)
    // node.empty() == true
}

insertが失敗したとき、つまりすでに同一のキーがコンテナーに存在したとき、insertedfalse, nodeinsertを呼び出す前のノードハンドルの値、positionはコンテナーの中の追加しようとしたキーに等しい要素を指す。insertに渡したノードハンドルは未規定の値になる。

int main()
{
    std::set<int> c = {1,2,3} ;
    auto n = c.extract(1) ;
    c.insert(1) ;

    auto [position, inserted, node] = c.insert( std::move(n) ) ;

    // nは未規定の値
    // inserted == false
    // nodeはinsert( std::move(n) )を呼び出す前のnの値
    // position == c.find(1)
}

規格はこの場合のnの値について規定していないが、最もありうる実装としては、nnodeにムーブされるので、nは空になり、ムーブ後の状態になる。

ノードハンドルの利用例

ノードハンドルの典型的な使い方は以下のとおり。

ストレージの再確保なしに、コンテナーの一部の要素だけ別のコンテナーに移す

int main()
{
    std::set<int> a = {1,2,3} ;
    std::set<int> b = {4,5,6} ;

    auto n = a.extract(1) ;
    b.insert( std::move(n) ) ;
}

コンテナーの寿命を超えて要素を存続させる

int main()
{
    std::set<int>::node_type n ;

    {
        std::set<int> c = {1,2,3} ;
        n = c.extract(1) ;
        // cが破棄される
    }

    // コンテナーの破棄後も存続する
    int value = n.value() ;
}

mapのキーを変更する

mapではキーは変更できない。キーを変更したければ、元の要素は削除して、新しい要素を追加する必要がある。これには動的なストレージの解放と確保が必要になる。

ノードハンドルを使えば、既存の要素のストレージに対して、所有権をmapから引き剥がした上で、キーを変更して、もう一度mapに差し戻すことができる。

int main()
{
    std::map< std::string, std::string > m =
    {
        {"cat", "meow"},
        {"DOG", "bow"}, // キーを間違えたので変更したい
        {"cow", "moo"}
    } ;

    // 所有権を引き剥がす
    auto n = m.extract("DOG") ;
    // キーを変更
    n.key() = "dog" ;
    // 差し戻す
    m.insert( std::move(n) ) ;
}

コンテナーアクセス関数

ヘッダーファイル<iterator>に、コンテナーアクセス関数として、フリー関数版のsize, empty, dataが追加された。それぞれ、メンバー関数のsize, empty, dataを呼び出す。

int main()
{
    std::vector<int> v ;

    std::size(v) ; // v.size()
    std::empty(v) ; // v.empty()
    std::data(v) ; // v.data() 
}

このフリー関数は配列やstd::initializer_list<T>にも使える。

int main()
{
    int a[10] ;

    std::size(a) ; // 10
    std::empty(a) ; // 常にfalse
    std::data(a) ; // a
}

clamp

template<class T>
constexpr const T&
clamp(const T& v, const T& lo, const T& hi);
template<class T, class Compare>
constexpr const T&
clamp(const T& v, const T& lo, const T& hi, Compare comp);

ヘッダーファイル<algorithm>に追加されたclamp(v, lo, hi)は値vloより小さい場合はloを、hiより高い場合はhiを、それ以外の場合はvを返す。

int main()
{
    std::clamp( 5, 0, 10 ) ; // 5
    std::clamp( -5, 0, 10 ) ; // 0
    std::clamp( 50, 0, 10 ) ; // 10
}

compを実引数に取るclampcompを値の比較に使う

clampには浮動小数点数も使えるが、NaNは渡せない。

3次元hypot

float hypot(float x, float y, float z);
double hypot(double x, double y, double z);
long double hypot(long double x, long double y, long double z);

ヘッダーファイル<cmath>に3次元のhypotが追加された。

戻り値

\[ \sqrt{x^2+y^2+z^2} \]

atomic<T>::is_always_lock_free

template < typename T >
struct atomic
{
    static constexpr bool is_always_lock_free = ... ;
} ;

C++17で<atomic>に追加されたatomic<T>::is_always_lock_freeは、atomic<T>の実装がすべての実行においてロックフリーであるとコンパイル時に保証できる場合、trueになるstatic constexprbool型のデータメンバーだ。

atomicには、他にもboolを返すメンバー関数is_lock_freeがあるが、これは実行時にロックフリーであるかどうかを判定できる。is_always_lock_freeはコンパイル時にロックフリーであるかどうかを判定できる。

scoped_lock : 可変長引数lock_guard

std::scoped_lockクラス<T ...>は可変長引数版のlock_guardだ。

int main()
{
    std::mutex a, b, c, d ;

    {
        // a,b,c,dをlockする
        std::scoped_lock l( a, b, c, d ) ;
        // a,b,c,dをunlockする
    }
}

std::scoped_lockのコンストラクターは複数のロックのオブジェクトのリファレンスを取り、それぞれにデッドロックを起こさない方法でメンバー関数lockを呼び出す。デストラクターはメンバー関数unlockを呼び出す。

std::byte

C++17ではバイトを表現する型としてstd::byteがライブラリに追加された。これは、コア言語の一部であり、別項で詳しく解説を行っている

最大公約数(gcd)と最小公倍数(lcm)

C++17ではヘッダーファイル<numeric>に最大公約数(gcd)と最小公倍数(lcm)が追加された。

int main()
{
    int a, b ;

    while( std::cin >> a >> b )
    {
        std::cout
            << "gcd: " << gcd(a,b)
            << "\nlcm: " << lcm(a,b) << '\n' ;
    }
}

gcd : 最大公約数

template <class M, class N>
constexpr std::common_type_t<M,N> gcd(M m, N n)
{
    if ( n == 0 )
        return m ;
    else
        return gcd( n, std::abs(m) % std::abs(n) ) ; 
}

gcd(m, n)mnがともにゼロの場合ゼロを返す。それ以外の場合、\(|m|\)\(|n|\)の最大公約数(Greatest Common Divisor)を返す。

lcm : 最小公倍数

template <class M, class N>
constexpr std::common_type_t<M,N> lcm(M m, N n)
{
    if ( m == 0 || n == 0 )
        return 0 ;
    else
        return std::abs(m) / gcd( m, n ) * std::abs(n) ;
}

lcm(m,n)は、mnのどちらかがゼロの場合ゼロを返す。それ以外の場合、\(|m|\)\(|n|\)の最小公倍数(Least Common Multiple)を返す。

ファイルシステム

ヘッダーファイル<filesystem>で定義されている標準ライブラリのファイルシステムは、ファイルやディレクトリーとその属性を扱うためのライブラリだ。

一般に「ファイルシステム」といった場合、たとえばLinuxのext4, Microsoft WindowsのFATやNTFS, Apple MacのHFS+やAPFSといったファイルとその属性を表現するためのストレージ上のデータ構造を意味する。C++の標準ライブラリのファイルシステムとは、そのようなファイルシステムを実現するデータ構造を操作するライブラリではない。ファイルシステムというデータ構造で抽象化された、ファイルやディレクトリーとその属性、それに付随する要素、たとえばパスやファイルやディレクトリーを操作するためのライブラリのことだ。

また、ファイルシステムライブラリでは、「ファイル」という用語は単に通常のファイルのみならず、ディレクトリー、シンボリックリンク、FIFO(名前付きパイプ)、ソケットなどの特殊なファイルも含む。

本書ではファイルシステムライブラリのすべてを詳細に解説していない。ファイルシステムライブラリは量が膨大なので、特定の関数の意味については、C++コンパイラーに付属のリファレンスマニュアルなどを参照するとよい。

名前空間

ファイルシステムライブラリはstd::filesystem名前空間スコープの下に宣言されている。

int main()
{
    std::filesystem::path p("/bin") ;
}

この名前空間は長いので、ファイルシステムライブラリを使うときは、関数のブロックスコープ単位でusingディレクティブを使うか、名前空間エイリアスを使って短い別名を付けるとよい。

void using_directive()
{
    // usingディレクティブ
    using namespace std::filesystem ;

    path p("/etc") ;
}

void namespace_alias()
{
    // 名前空間エイリアス
    namespace fs = std::filesystem ;

    fs::path p("/usr") ;
}

POSIX準拠

C++のファイルシステムのファイル操作の挙動は、POSIX規格に従う。実装によってはPOSIXに規定された挙動を提供できない場合もある。その場合は制限の範囲内で、できるだけPOSIXに近い挙動を行う。実装がどのような意味のある挙動も提供できない場合、エラーが通知される。

ファイルシステムの全体像

ファイルシステムライブラリの全体像を簡単に箇条書きすると以下のとおり。

エラー処理

ファイルシステムライブラリでエラーが発生した場合、エラーの通知方法には2種類の方法がある。例外を使う方法と、ヘッダーファイル<system_error>で定義されているエラー通知用のクラスstd::error_codeへのリファレンスを実引数として渡してエラー内容を受け取る方法だ。

エラー処理の方法は、エラーの起こる期待度によって選択できる。一般に、エラーがめったに起こらない場合、エラーが起こるのは予期していない場合、エラー処理には例外を使ったほうがよい。エラーが頻繁に起こる場合、エラーが起こることが予期できる場合、エラー処理には例外を使わないほうがよい。

例外

ファイルシステムライブラリの関数のうち、std::error_code &型を実引数に取らない関数は、以下のようにエラー通知を行う。

例外を使ったエラー処理は以下のとおり。

int main()
{
    using namespace std::filesystem ;

    try {
        // ファイル名から同じファイル名へのコピーによるエラー
        path file("foobar.txt") ;
        std::ofstream{ file } ;
        copy_file( file, file ) ;
       
    } catch( filesystem_error & e )
    { // エラーの場合
        auto path1 = e.path1() ; // 第一引数
        auto path2 = e.path2() ; // 第二引数
        auto error_code = e.code() ; // error_code
        
        std::cout
            << "error number: " << error_code.value ()
            << "\nerror message: " << error_code.message() 
            << "\npath1: " << path1
            << "\npath2: " << path2 << '\n' ;
    }
}

filesystem_errorは以下のようなクラスになっている。

namespace std::filesystem {
    class filesystem_error : public system_error {
    public:
        // 第一引数
        const path& path1() const noexcept;
        // 第二引数
        const path& path2() const noexcept;
        // エラー内容を人間が読めるnull終端文字列で返す
        const char* what() const noexcept override;
    };
}

非例外

ファイルシステムライブラリの関数のうち、std::error_code &型を実引数に取る関数は、以下のようにエラー通知を行う。

int main()
{
    using namespace std::filesystem ;

    // ファイル名から同じファイル名へのコピーによるエラー
    path file("foobar.txt") ;
    std::ofstream{ file } ;
    std::error_code error_code;
    copy_file( file, file, error_code ) ;

    if ( error_code )
    { // エラーの場合
        auto path1 = file ; // 第一引数
        auto path2 = file ; // 第二引数
        
        std::cout
            << "error number: " << error_code.value ()
            << "\nerror message: " << error_code.message() 
            << "\npath1: " << path1
            << "\npath2: " << path2 << '\n' ;
    }
}

path : ファイルパス文字列クラス

std::filesystem::pathはファイルパスを文字列で表現するためのクラスだ。文字列を表現するクラスとしてC++にはすでにstd::stringがあるが、ファイルパスという文字列を表現するために、別の専用クラスが作られた。

クラスpathは以下の機能を提供する。

pathはファイルパス文字列の表現と操作だけを提供するクラスで、物理ファイルシステムへの変更のコミットはしない。

ファイルパス文字列がどのように表現されているかは実装により異なる。POSIX環境では文字型をchar型としてUTF-8エンコードで表現するOSが多いが、Microsoft Windowsでは本書執筆現在、文字型をwchar_tとしてUTF-16エンコードで表現する慣習になっている。

また、OSによってはラテンアルファベットの大文字小文字を区別しなかったり、区別はするが無視されたりする実装もある。

クラスpathはそのようなファイルパス文字列の差異を吸収してくれる。

クラスpathには以下のようなネストされた型名がある。

namespace std::filesystem {
    class path {
    public:
        using value_type = see below ;
        using string_type = basic_string<value_type>;
        static constexpr value_type preferred_separator = see below ;
    } ;
}

value_typestring_typepathが内部でファイルパス文字列を表現するのに使う文字と文字列の型だ。preferred_separatorは、推奨されるディレクトリー区切り文字だ。たとえばPOSIX互換環境では/が用いられるが、Microsoft Windowsでは\が使われている。

ファイルパスの文字列

ファイルパスは文字列で表現する。C++の文字列のエンコードには以下のものがある。

path::value_typeがどの文字型を使い、どの文字列エンコードを使っているかは実装依存だ。pathはどの文字列エンコードが渡されても、path::value_typeの文字型と文字エンコードになるように自動的に変換が行われる。

int main()
{
    using namespace std::filesystem ;

    // ネイティブナローエンコード
    path p1( "/dev/null" ) ;
    // ネイティブワイドエンコード
    path p2( L"/dev/null" ) ;
    // UTF-16エンコード
    path p3( u"/dev/null" ) ;
    // UTF-32エンコード
    path p4( U"/dev/null" ) ;
}

なので、どの文字列エンコードで渡しても動く。

C++ではUTF-8エンコードの文字型はcharで、これはネイティブナローエンコードの文字型と同じなので、型システムによって区別できない。そのため、UTF-8文字列リテラルを渡すと、ネイティブナローエンコードとして認識される。

int main()
{
    using namespace std::filesystem ;

    // ネイティブナローエンコードとして解釈される
    path p( u8"ファイル名" ) ;
}

このコードは、ネイティブナローエンコードがUTF-8ではない場合、動く保証のない移植性の低いコードだ。UTF-8エンコードを移植性の高い方法でファイルパスとして使いたい場合、u8pathを使うとよい。

int main()
{
    using namespace std::filesystem ;

    // UTF-8エンコードとして解釈される
    // 実装の使う文字エンコードに変換される
    path = u8path( u8"ファイル名" ) ;
}

u8path(Source)SourceをUTF-8エンコードされた文字列として扱うので、通常の文字列リテラルを渡すと、ネイティブナローエンコードがUTF-8ではない環境では問題になる。

int main()
{
    using namespace std::filesystem ;

    // UTF-8エンコードとして解釈される
    // ネイティブナローエンコードがUTF-8ではない場合、問題になる
    path = u8path( "ファイル名" ) ;
}

u8pathを使う場合は、文字列は必ずUTF-8エンコードしなければならない。

環境によっては、ファイルパスに使える文字に制限があり、また特定の文字列は特別な意味を持つ予約語になっていることもあるので、移植性の高いプログラムの作成に当たってはこの点でも注意が必要だ。たとえば、環境によっては大文字小文字の区別をしないかもしれない。また、CONAUXのような文字列が特別な意味を持つかもしれない。

pathに格納されているファイルパス文字列を取得する方法は、環境依存の文字列エンコードとファイルパスの表現方法の差異により、さまざまな方法が用意されている。

ファイルパス文字列のフォーマットには以下の2つがある。

POSIX準拠の環境においては、ネイティブとジェネリックはまったく同じだ。POSIX準拠ではない環境では、ネイティブとジェネリックは異なるフォーマットを持つ可能性がある。

たとえば、Microsoft Windowsでは、ネイティブのファイルパス文字列はディレクトリーの区切り文字にPOSIX準拠の/ではなく\を使っている。

まずメンバー関数nativec_strがある。

class path {
{
public :
    const string_type& native() const noexcept;
    const value_type* c_str() const noexcept;
} ;

これはクラスpathが内部で使っている実装依存のネイティブな文字列型をそのまま返すものだ。

int main()
{
    using namespace std::filesystem ;

    path p = current_path() ;

    // 実装依存のbasic_stringの特殊化
    path::string_type str = p.native() ;

    // 実装依存の文字型
    path::value_type const * ptr = p.c_str() ;
    
}

このメンバー関数を使うコードは移植性に注意が必要だ。

strの型はpath::string_typeで、ptrの型は実装依存のpath::value_type const *だ。path::value_typeとpath::string_typeは、charwchar_t, std::stringstd::wstringのようなC++が標準で定義する型ではない可能性がある。

そして、path::string_typeへの変換関数operator string_type()がある。

int main()
{
    using namespace std::experimental::filesystem ;

    auto p = current_path() ;

    // 暗黙の型変換
    path::string_type str = p ;
}

pathoperator string_type()は、ネイティブの文字列型を既存のファイルストリームライブラリでオープンできる形式に変換して返す。たとえば空白文字を含むファイルパスのために、二重引用符で囲まれている文字列に変換されるかもしれない。

int main()
{
    using namespace std::filesystem ;

    path name("foo bar.txt") ;
    std::basic_ofstream<path::value_type> file( name ) ;
    file << "hello" ;
}

ネイティブのファイルパス文字列をstring, wstring, u16string, u32stringに変換して取得するメンバー関数に以下のものがある。

class path {
public :
    std::string string() const;
    std::wstring wstring() const;
    std::string u8string() const;
    std::u16string u16string() const;
    std::u32string u32string() const;
} ;

このうち、メンバー関数stringはネイティブナローエンコードされたstd::string, メンバー関数u8stringはUTF-8エンコードされたstd::stringを返す。

int main()
{
    using namespace std::filesystem ;

    path name("hello.txt") ;
    std::ofstream file( name.string() ) ;
    file << "hello" ;
}

ファイルパス文字列をジェネリックに変換して返すgeneric_string()系のメンバー関数がある。

class path {
public :
    std::string generic_string() const;
    std::wstring generic_wstring() const;
    std::string generic_u8string() const;
    std::u16string generic_u16string() const;
    std::u32string generic_u32string() const
} ;

使い方はネイティブな文字列を返すstring()系のメンバー関数と同じだ。

ファイルパスの文字列の文字型と文字列エンコードは環境ごとに異なるので、移植性の高いコードを書くときには注意が必要だ。

現実的には、モダンなPOSIX準拠の環境では、文字型はchar, 文字列型はstd::string, エンコードはUTF-8になる。

Microsoft WindowsのWin32サブシステムとMSVCはPOSIX準拠ではなく、本書執筆時点では、歴史的経緯により、文字型はwchar_t, 文字列型はstd::wstring, エンコードはUTF-16となっている。

ファイルパスの操作

クラスpathはファイルパス文字列の操作を提供している。std::stringとは違い、findsubstrのような操作は提供していないが、ファイルパス文字列に特化した操作を提供している。

operator /, operator /=はセパレーターで区切ったファイルパス文字列の追加を行う。

int main()
{
    using namespace std::filesystem ;

    path p("/") ;

    // "/usr"
    p /= "usr" ;
    // "/usr/local/include"
    p = p / "local" / "include" ;
}

operator +=は単なる文字列の結合を行う。

int main()
{
    using namespace std::filesystem ;

    path p("/") ;

    // "/usr"
    p += "usr" ;
    // "/usrlocal"
    p += "local" ;
    // "/usrlocalinclude"
    p += "include" ;
}

operator /と違い、operator +は存在しない。

その他にも、pathはさまざまなファイルパス文字列に対する操作を提供している。以下はその一例だ。

int main()
{
    using namespace std::filesystem ;

    path p( "/home/cpp/src/main.cpp" ) ;

    // "main.cpp"
    path filename = p.filename() ;
    // "main"
    path stem = p.stem() ;
    // ".cpp"
    path extension = p.extension() ;
    // "/home/cpp/src/main.o"
    p.replace_extension("o") ;
    // "/home/cpp/src/"
    p.remove_filename() ;
}

pathはファイルパス文字列に対してよく行う文字列処理を提供している。たとえばファイル名だけ抜き出す処理、拡張子だけ抜き出す処理、拡張子を変える処理などだ。

file_status

クラスfile_statusはファイルのタイプとパーミッションを保持するクラスだ。

ファイルのタイプとパーミッションはファイルパス文字列を指定して取得する方法が別途あるが、その方法では毎回物理ファイルシステムへのアクセスが発生する。file_statusはファイルのタイプとパーミッション情報を保持するクラスとして、いわばキャッシュの役割を果たす。

file_statusは物理ファイルシステムへの変更のコミットはしない。

file_statusクラスはstatus(path)もしくはstatus(path, error_code)で取得できる。あるいは、directory_entryのメンバー関数status()から取得できる。

タイプというのは、ファイルが種類を表すenumfile_typeで、通常のファイルやディレクトリーやシンボリックリンクといったファイルの種類を表す。

パーミッションというのは、ファイルの権限を表すビットマスクのenumpermsで、ファイルの所有者とグループと他人に対する読み込み、書き込み、実行のそれぞれの権限を表している。この値はPOSIXの値と同じになっている。

ファイルのタイプとパーミッションを取得するメンバー関数は以下のとおり。

class file_type {
public :
    file_type type() const noexcept;
    perms permissions() const noexcept;
} ;

以下のように使う。

int main()
{
    using namespace std::filesystem ;

    directory_iterator iter("."), end ;

    int regular_files = 0 ;
    int execs = 0 ;

    std::for_each( iter, end, [&]( auto entry )
    {
        auto file_status = entry.status() ;
        // is_regular_file( file_status )でも可
        if ( file_status.type() == file_type::regular )
            ++regular_files ;

        constexpr auto exec_bits = 
            perms::owner_exec | perms::group_exec | perms::others_exec ;

        auto permissions = file_status.permissions() ;
        if ( (  permissions != perms::unknown) &&
                (permissions & exec_bits) != perms::none ) 
            ++execs ;
    } ) ;

    std::cout
        << "Current directory has "
        << regular_files
        << " regular files.\n" ;
        << execs
        << " files are executable.\n" ;
}

このプログラムは、カレントディレクトリーにある通常のファイルの数と、実行可能なファイルの数を表示する。

ファイルパーミッションを表現するenumpermsは、パーミッションが不明な場合perms::unknownになる。この値は0xFFFFなのでビット演算をする場合には注意が必要だ。

それ以外のpermsの値はPOSIXに準拠しているが、permsscoped enum型なので、明示的なキャストが必要だ。

// エラー
std::filesystem::perms a = 0755 ;

// OK
std::filesystem::perms b = std::filesystem::perms(0755) ;

ファイルのタイプとパーミッションを書き換えるメンバー関数は以下のとおり。

void type(file_type ft) noexcept;
void permissions(perms prms) noexcept;

ただし、file_statusというのは単なるキャッシュ用のクラスなので、file_statusのタイプとパーミッションを「書き換える」というのは、単にfile_statusのオブジェクトに保持されている値を書き換えるだけで、物理ファイルシステムに反映されるものではない。物理ファイルシステムを書き換えるには、フリー関数のpermissionsを使う。

directory_entry

クラスdirectory_entryはファイルパス文字列を保持し、ファイルパスの指し示すファイルの情報を取得できるクラスだ。

物理ファイルシステムからファイルの情報を毎回読むのは非効率的だ。directory_entryはいわばファイル情報のキャッシュとしての用途を持つ。

directory_entryは物理ファイルシステムから情報を読み込むだけで、変更のコミットはしない。

directory_entryの構築は、コンストラクターに引数としてpathを与える他、directory_iteratorrecursive_directory_iteratorからも得ることができる。

int main()
{
    using namespace std::filesystem ;

    path p(".") ;

    // ファイルパス文字列から得る
    directory_entry e1(p) ;

    // イテレーターから得る
    directory_iterator i1(p) ;
    directory_entry e2 = *i1 ;

    recursive_directory_iterator i2(p) ;
    directory_entry e3 = *i2 ;
}

directory_entryにはさまざまなファイル情報を取得するメンバー関数があるが、これは同じ機能のものがフリー関数でも用意されている。directory_entryを使うと、ファイル情報をキャッシュできるため、同じファイルパスに対して、物理ファイルシステムの変更がないときに複数回のファイル情報取得を行うのが効率的になる。

int main()
{
    using namespace std::filesystem ;

    directory_entry entry("/home/cpp/foo") ;

    // 存在確認
    bool b = entry.exists() ;

    // "/home/cpp/foo"
    path p = entry.path() ;
    file_status s = entry.status() ;

    // ファイルサイズを取得
    std::uintmax_t size = entry.file_size() ;

    {
        std::ofstream foo( entry.path() ) ;
        foo << "hello" ;
    }

    // 物理ファイルシステムから情報を更新
    entry.refresh() ;
    // もう一度ファイルサイズを取得
    size = entry.file_size() ;

    // 情報を取得するファイルパスを
    // "/home/cpp/bar"
    // に置き換えてrefresh()を呼び出す
    entry.replace_filename("bar") ;
}

directory_entryはキャッシュ用のクラスで、自動的に物理ファイルシステムの変更に追随しないので、最新の情報を取得するには、明示的にメンバー関数refreshを呼び出す必要がある。

directory_iterator

directory_iteratorは、あるディレクトリー下に存在するファイルパスをイテレーターの形式で列挙するためのクラスだ。

たとえば、カレントディレクトリー下のファイルパスをすべて列挙するコードは以下のようになる。

int main()
{
    using namespace std::filesystem ;
    directory_iterator iter("."), end ;
    std::copy( iter, end,
        std::ostream_iterator<path>(std::cout, "\n") ) ;
}

directory_iteratorはコンストラクターとしてpathを渡すと、そのディレクトリー下の最初のファイルに相当するdirectory_entryを返すイテレーターとなる。コンストラクターで指定されたディレクトリー下にファイルが存在しない場合、終端イテレーターになる。

directory_iteratorのデフォルトコンストラクターは終端イテレーターになる。終端イテレーターはデリファレンスできない。

directory_iterator::value_typedirectory_entryで、イテレーターのカテゴリーは入力イテレーターとなる。

directory_iteratorはカレントディレクトリー(.)と親ディレクトリー(..)は列挙しない。

directory_iteratorがディレクトリー下のファイルをどのような順番で列挙するかは未規定だ。

directory_iteratorによって返されるファイルパスは存在しない可能性があるので、ファイルが存在することを当てにしてはいけない。たとえば、存在しないファイルへのシンボリックリンクかもしれない。

directory_iteratorのオブジェクトが作成された後に物理ファイルシステムになされた変更は、反映されるかどうか未規定である。

directory_iteratorのコンストラクターは列挙時の動作を指定できるdirectory_optionsを実引数に受け取ることができる。しかし、C++17の標準規格の範囲ではdirectory_iteratorの挙動を変更するdirectory_optionsは規定されていない。

エラー処理

directory_iteratorは構築時にエラーが発生することがある。このエラーを例外ではなくerror_codeで受け取りたい場合、コンストラクターの実引数でerror_codeへのリファレンスを渡す。

int main()
{
    using namespace std::filesystem ;

    std::error_code err ;

    directory_iterator iter("this-directory-does-not-exist", err) ;

    if ( err )
    {
        // エラー処理
    }
}

directory_iteratorはインクリメント時にエラーが発生することがある。このエラーを例外ではなくerror_codeで受け取りたい場合、メンバー関数incrementを呼び出す。

int main()
{
    using namespace std::experimental::filesystem ; 

    recursive_directory_iterator iter("."), end ;

    std::error_code err ;

    for ( ; iter != end && !err ; iter.increment( err ) )
    {
        std::cout << *iter << "\n" ;
    }

    if ( err )
    {
        // エラー処理
    }
}

recursive_directory_iterator

recursive_directory_iteratorは指定されたディレクトリー下に存在するサブディレクトリーの下も含めて、すべてのファイルを列挙する。使い方はdirectory_iteratorとほぼ同じだ。

int main()
{
    using namespace std::filesystem ; 
    recursive_directory_iterator iter("."), end ;

    std::copy(  iter, end,
                std::ostream_iterator<path>(std::cout, "\n") ) ;
}

メンバー関数options, depth, recursion_pending, pop, disable_recursion_pendingをデリファレンスできないイテレーターに対して呼び出した際の挙動は未定義だ。

オプション

recursive_directory_iteratorはコンストラクターの実引数にdirectory_options型のscoped enum値を取ることによって、挙動を変更できる。directory_options型のenum値はビットマスクになっていて、以下の3つのビットマスク値が規定されている。

名前 意味
none デフォルト。ディレクトリーシンボリックリンクをスキップ。パーミッション違反はエラー
follow_directory_symlink ディレクトリーシンボリックリンクの中も列挙
skip_permission_denied パーミッション違反のディレクトリーはスキップ

このうち取りうる組み合わせは、none, follow_directory_symlink, skip_permission_denied, follow_directory_symlink | skip_permission_deniedの4種類になる。

int main()
{
    using namespace std::filesystem ; 
    recursive_directory_iterator
        iter("/", directory_options::skip_permission_denied), end ;

    std::copy(  iter, end,
                std::ostream_iterator<path>(std::cout, "\n") ) ;
}

follow_directory_symlinkは、親ディレクトリーへのシンボリックリンクが存在する場合、イテレーターが終端イテレーターに到達しない可能性があるので注意すること。

int main()
{
    using namespace std::filesystem ;

    // 自分自身を含むディレクトリーに対するシンボリックリンク
    create_symlink(".", "foo") ;

    recursive_directory_iterator
        iter(".", directory_options::follow_directory_symlink), end ;

    // エラー、もしくは終了しない
    std::copy( iter, end, std::ostream_iterator<path>(std::cout) ) ;
}

recursive_directory_iteratorの現在のdirectory_optionsを得るには、メンバー関数optionsを呼ぶ。

class recursive_directory_iterator {
public :
    directory_options options() const ;
} ;

depth : 深さ取得

recursive_directory_iteratorが現在列挙しているディレクトリーの深さを知るには、メンバー関数depthを呼ぶ。

class recursive_directory_iterator {
public :
    int depth() const ;
} ;

最初のディレクトリーの深さは0で、次のサブディレクトリーの深さは1、それ以降のサブディレクトリーも同様に続く。

pop : 現在のディレクトリーの列挙中止

メンバー関数popを呼ぶと、現在列挙中のディレクトリーの列挙を取りやめ、親ディレクトリーに戻る。現在のディレクトリーが初期ディレクトリーの場合、つまりdepth() == 0の場合は、終端イテレーターになる。

class recursive_directory_iterator {
public :
    void pop();
    void pop(error_code& ec);
} ;

たとえば、カレントディレクトリーが以下のようなディレクトリーツリーで、イテレーターが以下に書かれた順番でファイルを列挙する環境の場合、

a
b
b/a
b/c
b/d
c
d

以下のようなプログラムを実行すると、

int main()
{
    std::filesystem ;

    recursive_directory_iterator iter("."), end ;

    auto const p = canonical("b/a") ;

    for ( ; iter != end ; ++iter )
    {
        std::cout << *iter << '\n' ;

        if ( canonical(iter->path()) == p )
            iter.pop() ;
    }
}

標準出力が指すファイルとその順番は以下のようになる。

a
b
b/a
c
d

"b/a"に到達した時点でpop()が呼ばれるので、それ以上のディレクトリーb下の列挙が中止され、親ディレクトリーであるカレントディレクトリーに戻る。

recursion_pending : 現在のディレクトリーの再帰をスキップ

disable_recursion_pendingは現在のディレクトリーの下を再帰的に列挙することをスキップする機能だ。

class recursive_directory_iterator {
public :
    bool recursion_pending() const ;
    void disable_recursion_pending() ;
} ;

recursion_pending()は、直前のイテレーターのインクリメント操作の後にdisable_recursion_pending()が呼ばれていない場合、trueを返す。そうでない場合はfalseを返す。

言い換えれば、disable_recursion_pending()を呼んだ直後で、まだイテレーターのインクリメント操作をしていない場合、recursion_pending()falseを返す。

int main()
{
    using namespace std ;
    recursive_directory_iterator iter("."), end ;

    // true
    bool b1 = iter.recursion_pending() ;

    iter.disable_recursion_pending() ;
    // false
    bool b2 = iter.recursion_pending() ;

    ++iter ;
    //  true
    bool b3 = iter.recursion_pending() ;


    iter.disable_recursion_pending() ;
    // false
    bool b4 = iter.recursion_pending() ;
}

現在recursive_directory_iteratorが指しているファイルパスがディレクトリーである場合、そのイテレーターをインクリメントすると、そのディレクトリー下を再帰的に列挙することになる。しかし、recursion_pending()falseを返す場合、ディレクトリーの最適的な列挙はスキップされる。インクリメント操作が行われた後はrecursion_pending()の結果はtrueに戻る。

つまり、disable_recursion_pendingは、現在指しているディレクトリー下を再帰的に列挙することをスキップする機能を提供する。

たとえば、カレントディレクトリーが以下のようなディレクトリーツリーで、イテレーターが以下に書かれた順番でファイルを列挙する環境の場合、

a
b
b/a
b/c
b/d
c
d

以下のようなプログラムを実行すると、

int main()
{
    std::filesystem ;

    recursive_directory_iterator iter("."), end ;

    auto const p = canonical("b/a") ;

    for ( ; iter != end ; ++iter )
    {
        std::cout << *iter << '\n' ;

        if ( iter->is_directory() )
            iter.disable_recursion_pending() ;
    }
}

標準出力が指すファイルとその順番は以下のようになる。

a
b
c
d

このプログラムはディレクトリーであれば必ずdisable_recursion_pending()が呼ばれるので、サブディレクトリーの再帰的な列挙は行われず、結果的に動作はdirectory_iteratorと同じになる。

disable_recursion_pendingを呼び出すことによって、選択的にディレクトリーの再帰的な列挙をスキップさせることができる。

ファイルシステム操作関数

ファイルパス取得

current_path

path current_path();
path current_path(error_code& ec);

カレント・ワーキング・ディレクトリー(current working directory)への絶対パスを返す。

temp_directory_path

path temp_directory_path();
path temp_directory_path(error_code& ec);

一時ファイルを作成するのに最適な一時ディレクトリー(temporary directory)へのファイルパスを返す。

ファイルパス操作

absolute

path absolute(const path& p);
path absolute(const path& p, error_code& ec);

pへの絶対パスを返す。pの指すファイルが存在しない場合の挙動は未規定。

canonical

path canonical(const path& p, const path& base = current_path());
path canonical(const path& p, error_code& ec);
path canonical(const path& p, const path& base, error_code& ec);

存在するファイルへのファイルパスpへの、シンボリックリンク、カレントディレクトリー(.)、親ディレクトリー(..)の存在しない絶対パスを返す。

weakly_canonical

path weakly_canonical(const path& p);
path weakly_canonical(const path& p, error_code& ec);

ファイルパスpのシンボリックリンクが解決され、正規化されたパスを返す。ファイルパスの正規化についての定義は長くなるので省略。

relative

path relative(const path& p, error_code& ec);
path relative(const path& p, const path& base = current_path());
path relative(const path& p, const path& base, error_code& ec);

ファイルパスbaseからファイルパスpに対する相対パスを返す。

proximate

path proximate(const path& p, error_code& ec);
path proximate(const path& p, const path& base = current_path());
path proximate(const path& p, const path& base, error_code& ec);

ファイルパスbaseからのファイルパスpに対する相対パスが空パスでなければ相対パスを返す。相対パスが空パスならばpが返る。

作成

create_directory

bool create_directory(const path& p);
bool create_directory(const path& p, error_code& ec) noexcept;

pの指すディレクトリーを1つ作成する。新しいディレクトリーが作成できた場合はtrueを、作成できなかった場合はfalseを返す。pが既存のディレクトリーを指していて新しいディレクトリーが作成できなかった場合はエラーにはならない。単にfalseが返る。

bool create_directory(
    const path& p, const path& existing_p);

bool create_directory(
    const path& p, const path& existing_p,
    error_code& ec) noexcept;

新しく作成するディレクトリーpのアトリビュートを既存のディレクトリーexisting_pと同じものにする。

create_directories

bool create_directories(const path& p);
bool create_directories(const path& p, error_code& ec) noexcept;

ファイルパスpの中のディレクトリーで存在しないものをすべて作成する。

以下のプログラムは、カレントディレクトリーの下のディレクトリーaの下のディレクトリーbの下にディレクトリーcを作成する。もし、途中のディレクトリーであるa, bが存在しない場合、それも作成する。

int main()
{
    using namespace std::filesystem ;
    create_directories("./a/b/c") ;
}

戻り値は、ディレクトリーを作成した場合true, そうでない場合false

void create_directory_symlink(
    const path& to, const path& new_symlink);
void create_directory_symlink(
    const path& to, const path& new_symlink,
    error_code& ec) noexcept;

ディレクトリーtoに解決されるシンボリックリンクnew_symlinkを作成する。

一部のOSでは、ディレクトリーへのシンボリックリンクとファイルへのシンボリックリンクを作成時に明示的に区別する必要がある。ポータブルなコードはディレクトリーへのシンボリックリンクを作成するときにはcreate_symlinkではなくcreate_directory_symlinkを使うべきである。

一部のOSはシンボリックリンクをサポートしていない。ポータブルなコードでは注意すべきである。

void create_symlink(
    const path& to, const path& new_symlink);
void create_symlink(
    const path& to, const path& new_symlink,
    error_code& ec) noexcept;

ファイルパスtoに解決されるシンボリックリンクnew_symlinkを作成する。

void create_hard_link(
    const path& to, const path& new_hard_link);
void create_hard_link(
    const path& to, const path& new_hard_link,
    error_code& ec) noexcept;

ファイルパスtoに解決されるハードリンクnew_hard_linkを作成する。

コピー

copy_file

bool copy_file( const path& from, const path& to);
bool copy_file( const path& from, const path& to,
                error_code& ec) noexcept;
bool copy_file( const path& from, const path& to,
                copy_options options);
bool copy_file( const path& from, const path& to,
                copy_options options,
                error_code& ec) noexcept;

ファイルパスfromのファイルをファイルパスtoにコピーする。

copy_optionsはコピーの挙動を変えるビットマスクのenum型で、以下のenum値がサポートされている。

名前 意味
none デフォルト、ファイルがすでに存在する場合はエラー
skip_existing 既存のファイルを上書きしない。スキップはエラーとして報告しない
overwrite_existing 既存のファイルを上書きする
update_existing 既存のファイルが上書きしようとするファイルより古ければ上書きする

copy

void copy(  const path& from, const path& to);
void copy(  const path& from, const path& to,
            error_code& ec) noexcept;
void copy(  const path& from, const path& to,
            copy_options options);
void copy(  const path& from, const path& to,
            copy_options options,
            error_code& ec) noexcept;

ファイルパスfromのファイルをファイルパスtoにコピーする。

copy_optionsはコピーの挙動を変えるビットマスク型のenum型で、以下のenum値がサポートされている。

名前 意味
none デフォルト、サブディレクトリーはコピーしない
recursive サブディレクトリーとその中身もコピーする
名前 意味
none デフォルト、シンボリックリンクをフォローする
copy_symlinks シンボリックリンクをシンボリックリンクとしてコピーする。シンボリックリンクが指すファイルを直接コピーしない
skip_symlinks シンボリックリンクを無視する
名前 意味
none デフォルト、ディレクトリー下の中身をコピーする
directories_only ディレクトリー構造のみをコピーする。非ディレクトリーファイルはコピーしない
create_symlinks ファイルをコピーするのではなく、シンボリックリンクを作成する。コピー先がカレントディレクトリーではない場合、コピー元のファイルパスは絶対パスでなければならない
create_hard_links ファイルをコピーするのではなく、ハードリンクを作成する
void copy_symlink(  const path& existing_symlink,
                    const path& new_symlink);
void copy_symlink(  const path& existing_symlink,
                    const path& new_symlink,
                    error_code& ec) noexcept;

existing_symlinknew_symlinkにコピーする。

削除

remove

bool remove(const path& p);
bool remove(const path& p, error_code& ec) noexcept;

ファイルパスpの指すファイルが存在するのであれば削除する。ファイルがシンボリックリンクの場合、シンボリックリンクファイルが削除される。フォロー先は削除されない。

戻り値として、ファイルが存在しない場合falseを返す。それ以外の場合trueを返す。error_codeでエラー通知を受け取る関数オーバーロードでは、エラーならばfalseが返る。

remove_all

uintmax_t remove_all(const path& p);
uintmax_t remove_all(const path& p, error_code& ec) noexcept;

ファイルパスpの下の存在するファイルをすべて削除した後、pの指すファイルも削除する。

つまり、pがディレクトリーファイルを指していて、そのディレクトリー下にサブディレクトリーやファイルが存在する場合、それらがすべて削除され、ディレクトリーpも削除される。

pがディレクトリーではないファイルを指す場合、pが削除される。

戻り値として、削除したファイルの個数が返る。error_codeでエラー通知を受け取る関数オーバーロードの場合、エラーならばstatic_cast<uintmax_t>(-1)が返る。

変更

permissions

void permissions(   const path& p, perms prms,
                    perm_options opts=perm_options::replace);
void permissions(   const path& p, perms prms,
                    error_code& ec) noexcept;
void permissions(   const path& p, perms prms,
                    perm_options opts,
                    error_code& ec);

ファイルパスpのパーミッションを変更する。

optsperm_options型のenum値、replace, add, removeのうちいずれか1つと、別途nofollowを指定することができる。省略した場合はreplaceになる。

カレントディレクトリーに存在するファイルfooを、すべてのユーザーに対して実行権限を付加するには、以下のように書く。

int main()
{
    using namespace std::filesystem ;

    permissions( "./foo", perms(0111), perm_options::add ) ;
}

perm_optionsは以下のようなenum値を持つ。

名前 意味
replace ファイルのパーミッションをprmsで置き換える
add ファイルのパーミッションにprmsで指定されたものを追加する
remove ファイルのパーミッションからprmsで指定されたものを取り除く
nofollow ファイルがシンボリックリンクの場合、シンボリックリンクのフォロー先のファイルではなく、シンボリックリンクそのもののパーミッションを変更する

たとえば、パーミッションを置き換えつつ、シンボリックリンクそのもののパーミッションを書き換えたい場合は、

perm_options opts = perm_options::replace | perm_options::nofollow ;

と書く。

rename

void rename(const path& old_p, const path& new_p);
void rename(const path& old_p, const path& new_p,
            error_code& ec) noexcept;

ファイルold_pをファイルnew_pにリネームする。

old_pnew_pが同じ存在するファイルを指す場合、何もしない。

int main()
{
    using namespace std:filesystem ;

    // 何もしない
    rename("foo", "foo") ;
}

それ以外の場合、リネームに伴って以下のような挙動も発生する。

もし、リネーム前にnew_pが既存のファイルを指していた場合、リネームに伴ってnew_pは削除される。

int main()
{
    using namespace std::experimental::filesystem ;

    {
        std::ofstream old_p("old_p"), new_p("new_p") ;

        old_p << "old_p" ;
        new_p << "new_p" ;
    }

    // ファイルold_pの内容は"old_p"
    // ファイルnew_pの内容は"new_p"

    // ファイルold_pをnew_pにリネーム
    // もともとのnew_pは削除される
    rename("old_p", "new_p") ;

    std::ifstream new_p("new_p") ;

    std::string text ;
    new_p >> text ;

    // "old_p"
    std::cout << text ;
}

もし、new_pが既存の空ディレクトリーを指していた場合、POSIX準拠OSであれば、リネームに伴ってnew_pは削除される。他のOSではエラーになるかもしれない。

int main()
{
    using namespace std::experimental::filesystem ;

    create_directory("old_p") ;
    create_directory("new_p") ;

    // POSIX準拠環境であればエラーにならないことが保証される
    rename("old_p", "new_p") ;
}

old_pがシンボリックリンクの場合、フォロー先ではなくシンボリックリンクファイルがリネームされる。

resize_file

void resize_file(   const path& p, uintmax_t new_size);
void resize_file(   const path& p, uintmax_t new_size,
                    error_code& ec) noexcept;

ファイルパスpathの指すファイルのファイルサイズをnew_sizeにする。

リサイズはPOSIXのtruncate()で行われたかのように振る舞う。つまり、ファイルを小さくリサイズした場合、余計なデータは捨てられる。ファイルを大きくリサイズした場合、増えたデータはnullバイト(\0)でパディングされる。ファイルの最終アクセス日時も更新される。

情報取得

ファイルタイプの判定

ファイルタイプを表現するfile_type型のenumがあり、そのenum値は以下のようになっている。

名前 意味
none ファイルタイプが決定できないかエラー
not_found ファイルが発見できなかったことを示す疑似ファイルタイプ
regular 通常のファイル
directory ディレクトリーファイル
symlink シンボリックリンクファイル
block ブロックスペシャルファイル
fifo FIFOもしくはパイプファイル
socket ソケットファイル
unknown ファイルは存在するがファイルタイプは決定できない

この他に、実装依存のファイルタイプが追加されている可能性がある。

ファイルタイプを調べるには、file_statusのメンバー関数typeの戻り値を調べればよい。

以下のプログラムは、カレントディレクトリーに存在するファイルfooがディレクトリーかどうかを調べるコードだ。

int main()
{
    using namespace std::filesystem ;

    auto s = status("./foo") ;
    bool b = s.type() == file_type::directory ;
}

また、statusもしくはpathからファイルタイプがディレクトリーであるかどうかを判定できるis_directoryも用意されている。

int main()
{
    using namespace std::filesystem ;

    bool b1 = is_directory("./foo") ;

    auto s = status("./foo") ;
    bool b2 = is_directory(s) ;
}

file_statusはファイル情報をキャッシュするので、物理ファイルシステムに変更を加えない状態で、同じファイルに対して何度もファイル情報を取得する場合は、file_statusを使ったほうがよい。

このようなis_xという形式のフリー関数は、いずれも以下の形式を取る。

bool is_x(file_status s) noexcept;
bool is_x(const path& p);
bool is_x(const path& p, error_code& ec) noexcept;

以下はフリー関数の名前と、どのファイルタイプであるかを判定する表だ。

名前 意味
is_regular_file 通常のファイル
is_directory ディレクトリーファイル
is_symlink シンボリックリンクファイル
is_block ブロックスペシャルファイル
is_fifo FIFOもしくはパイプファイル
is_socket ソケットファイル

また、単一のファイルタイプを調べるのではない以下のような名前のフリー関数が存在する。

名前 意味
is_other ファイルが存在し、通常のファイルでもディレクトリーでもシンボリックリンクでもないタイプ
is_empty ファイルがディレクトリーの場合、ディレクトリー下が空であればtrueを返す。
ファイルが非ディレクトリーの場合、ファイルサイズが0であればtrueを返す。

status

file_status status(const path& p);
file_status status(const path& p, error_code& ec) noexcept;

ファイルパスpのファイルの情報を格納するfile_statusを返す。

pがシンボリックリンクの場合、フォロー先のファイルのfile_statusを返す。

status_known

bool status_known(file_status s) noexcept;

s.type() != file_type::noneを返す。

file_status symlink_status(const path& p);
file_status symlink_status(const path& p, error_code& ec) noexcept;

statusと同じだが、pがシンボリックリンクの場合、そのシンボリックリンクファイルのstatusを返す。

equivalent

bool equivalent(const path& p1, const path& p2);
bool equivalent(const path& p1, const path& p2,
                error_code& ec) noexcept;

p1p2が物理ファイルシステム上、同一のファイルである場合、trueを返す。そうでない場合falseを返す。

exists

bool exists(file_status s) noexcept;
bool exists(const path& p);
bool exists(const path& p, error_code& ec) noexcept;

s, pが指すファイルが存在するのであればtrueを返す。そうでない場合falseを返す。

file_size

uintmax_t file_size(const path& p);
uintmax_t file_size(const path& p, error_code& ec) noexcept;

pの指すファイルのファイルサイズを返す。

ファイルが存在しない場合エラーとなる。ファイルが通常のファイルの場合、ファイルサイズを返す。それ以外の場合、挙動は実装依存となる。

エラー通知をerror_codeで受け取る関数オーバーロードでエラーのとき、戻り値はstatic_cast<uintmax_t>(-1)となる。

uintmax_t hard_link_count(const path& p);
uintmax_t hard_link_count(const path& p, error_code& ec) noexcept;

pの指すファイルのハードリンク数を返す。

エラー通知をerror_codeで受け取る関数オーバーロードでエラーのとき、戻り値はstatic_cast<uintmax_t>(-1)となる。

last_write_time

file_time_type last_write_time( const path& p);
file_time_type last_write_time( const path& p,
                                error_code& ec) noexcept;

pの指すファイルの最終更新日時を返す。

void last_write_time(   const path& p, file_time_type new_time);
void last_write_time(   const path& p, file_time_type new_time,
                        error_code& ec) noexcept;

pの指すファイルの最終更新日時をnew_timeにする。

last_write_time(p, new_time)を呼び出した後に、last_write_time(p) == new_timeである保証はない。なぜならば、物理ファイルシステムの実装に起因する時刻の分解能や品質の問題があるからだ。

file_time_typeは、std::chrono_time_pointの特殊化で以下のように定義されている。

namespace std::filesystem {
    using file_time_type = std::chrono::time_point< trivial-clock > ;
}

trivial-clockとは、クロック(より正確にはTrivialClock)の要件を満たすクロックで、ファイルシステムのタイムスタンプの値を正確に表現できるものとされている。クロックの具体的な型は実装依存なので、完全にポータブルなコードではファイルシステムで時間を扱うのは極めて困難になる。せいぜい現在時刻を設定するとか、差分の時間を設定するぐらいしかできない。

int main()
{
    using namespace std::experimental::filesystem ;
    using namespace std::chrono ;
    using namespace std::literals ;

    // 最終更新日時を取得
    auto timestamp = last_write_time( "foo" ) ;

    // 時刻を1時間進める
    timestamp += 1h ;
    // 更新
    last_write_time( "foo", timestamp ) ;


    // 現在時刻を取得
    auto now = file_time_type::clock::now() ;

    last_write_time( "foo", now ) ;
}

ただし、多くの実装ではfile_time_typeとして、time_point<std::chrono::system_clock>が使われている。file_time_type::clocksystem_clockであれば、system_clock::to_time_tsystem_clock::from_time_tによってtime_t型との相互変換ができるために、いくぶんマシになる。

// file_time_type::clockがsystem_clockである場合

int main()
{
    using namespace std::experimental::filesystem ;
    using namespace std::chrono ;

    // 最終更新日時を文字列で得る
    auto time_point_value = last_write_time( "foo" ) ;
    time_t time_t_value =
        system_clock::to_time_t( time_point_value ) ;
    std::cout << ctime( &time_t_value ) << '\n' ;

   
    // 最終更新日時を2017-10-12 19:02:58に設定
    tm struct_tm{} ;
    struct_tm.tm_year = 2017 - 1900 ;
    struct_tm.tm_mon = 10 ;
    struct_tm.tm_mday = 12 ;
    struct_tm.tm_hour = 19 ;
    struct_tm.tm_min = 2 ;
    struct_tm.tm_sec = 58 ;

    time_t timestamp = std::mktime( &struct_tm ) ;
    auto tp = system_clock::from_time_t( timestamp ) ;

    last_write_time( "foo", tp ) ;
}

あまりマシになっていないように見えるのは、C++では現在<chrono>から利用できるC++風のモダンなカレンダーライブラリがないからだ。この問題は将来の規格改定で改善されるだろう。

path read_symlink(const path& p);
path read_symlink(const path& p, error_code& ec);

シンボリックリンクpの解決される先のファイルパスを返す。

pがシンボリックリンクではない場合はエラーになる。

space

space_info space(const path& p);
space_info space(const path& p, error_code& ec) noexcept;

ファイルパスpが指す先の容量を取得する。

クラスspace_infoは以下のように定義されている。

struct space_info {
    uintmax_t capacity;
    uintmax_t free;
    uintmax_t available;
};

この関数は、POSIXのstatvfs関数を呼び出した結果のstruct statvfsf_blocks, f_bfree, f_bavailメンバーを、それぞれf_frsizeで乗じて、space_infoのメンバーcapacity, free, availableとして返す。値の決定できないメンバーにはstatic_cast<uintmax_t>(-1)が代入される。

エラー通知をerror_codeで返す関数オーバーロードがエラーの場合、space_infoのメンバーにはすべてstatic_cast<uintmax_t>(-1)が代入される。

space_infoのメンバーの意味をわかりやすく説明すると、以下の表のようになる。

名前 意味
capacity 総容量
free 空き容量
available 権限のないユーザーが使える空き容量