巻末: C++14の新機能
C++14は、2014年に制定されたC++11の次のC++標準規格である。C++14の位置づけとしてはマイナーアップデートで、新機能の追加は少ない。
二進数リテラル(binary literal)
二進数リテラルは、整数リテラルである。
int a = 0b1011 ; // 11
inb b = 0b10000000 ; // 128
プレフィクス0b、もしくは0Bに続いて'1'か'0'が続く整数リテラルは、二進数リテラルである。
二進数リテラルは、浮動小数点数リテラルではない。
double d = 0b111.11 ; // エラー
整数リテラルのサフィックスは、二進数リテラルにも適用できる。
auto a = 0b0u ; // unsigned int
auto b = 0b0l ; // long
プログラミングで整数を扱うときに、二進数で考えたい場合がよくあるが、C++と、その土台となったC言語には、二進数リテラルが存在しなかった。C++14では、ようやく二進数リテラルが追加された。
桁区切り(digit separator)
桁区切り(digit deparator)とは、整数リテラルと浮動小数点数リテラルの桁を区切ることができる文法である。
区切り文字には、シングルクオート' を使う。
int i = 1'000'000 ;
double d = 3.14159'26535'89793 ;
std::uint32_t bits = 0b11001111'01111000'11111011'11001101 ;
std::uint32_t hex = 0xde'ad'be'af ;
区切る桁数は任意である。
int x = 1'0'00'000'0000'00'0 ;
1000000と10000000でどちらが大きいか、また具体的にいくつなのか。人間の目では判断しにくい。1'000'000と10'000'000ならば、人間の目にも判断しやすい。
桁区切りはソースコード中の数値表記を、人間の目に読みやすくしてくれる。コードの意味上の違いはない。
汎用lambdaキャプチャー(Generalized Lambda-capture)
汎用lambdaキャプチャー(generalized lambda capture)、あるいは、初期化キャプチャー(init-capture)は、C++14で追加された新しいlambdaキャプチャーである。
初期化キャプチャー: 識別子 初期化子 & 識別子 初期化子
以下のように書く。
[ x = 0 ](){ return x ; } ;
これによって、lambda式によるキャプチャーがより便利になる。
汎用lambdaキャプチャーは、lambda式によって生成されるクロージャー型のデータメンバーとその初期化子を書くシンタックスシュガーだと考えることができる。
struct closure_type
{
private :
int x ;
public :
closure_type( int init )
: x( init ) { }
auto operator ( ) () const
{
return x ;
}
} ;
初期化キャプチャーの型は"auto 初期化キャプチャー"と書いた時の変数の型と同等である。
// aの型は、
// auto a = expr ;
// と書いた時の型に同じ
[ a = expr ]{ } ;
// auto & b = expr
[ & b = expr ] { } ;
初期化キャプチャーの用途は様々だが、機能追加にあたって主張された用途としては、クラスの非staticデータメンバーのコピーキャプチャーだ。
C++11のlambda式では、クラスの非staticデータメンバーはキャプチャーできない。
struct X
{
int m ;
void f()
{
[m]{ return m ; } ; // エラー
[=]{ return m ; } ; // #1 OK、ただし・・・
}
} ;
lambda式がキャプチャーしているのは、thisポインターである。#1は、以下のように書くのと等しい。
void X::f()
{
[this]{ return this->m ; } ;
}
したがって、クラスの非staticデータメンバーは、thisポインターを経由した間接的なアクセスをしているのであって、実質はリファレンスキャプチャーといえる。このことは、クラスのメンバー関数からクロージャーオブジェクトを返す際に問題になる。
struct X
{
int m ;
auto get_closure()
{
return [=]{ return m ; } ;
}
} ;
int main()
{
std::function< int() > f ;
{ // ブロックスコープ開始
X x{} ;
f = x.get_closure() ;
// ブロックスコープ終了
} // xはすでに破棄されている
// クロージャーオブジェクトを呼び出す
f() ; // 実行時エラー
}
上記のコードは、すでに破棄されたオブジェクトを参照しているので、挙動は未定義である。
C++11では、この場合にmをコピーキャプチャーするには、まずローカル変数を宣言しなければならなかった。
auto X::get_closure()
{
int m = this->m ;
return [m]{ return m; }
}
初期化キャプチャーにより、以下のように書ける。
auto X::get_closure()
{
return [ m = m ] { return m ; } ;
}
また、クロージャーオブジェクトにムーブすることもできるようになった。
auto f()
{
std::vector<int> v { 1,2,3,4,5 } ;
return [ v = std::move(v) ] { ... } ;
// これ以降vは使わない
}
ここで、従来のコピーキャプチャを使うと、コピーされてしまう。初期化キャプチャーを使うことにより、ムーブすることができる。
その他、変数の名前が長すぎるときに、短い名前をつけることもできる。
void f()
{
int really_long_name = 0 ;
[ & x = really_long_name ] { ... } ;
}
ジェネリックlambda
ジェネリックlambdaは、lambda式の仮引数の型を省略できる機能である。型指定子としてautoを書くと、ジェネリックlambdaとなる。
int main()
{
auto f = []( auto x ) { return x ; } ;
f( 0 ) ;
f( 0.0 ) ;
f( 'A' ) ;
f( "hello" ) ;
}
実装としては、クロージャー型のoperator ()をメンバーテンプレートにするシンタックスシュガーだと考えることができる。
struct closure_type
{
template < typename T >
auto operator () ( T x ) const
{
return x ;
}
} ;
int main()
{
closure_type f ;
f( 0 ) ;
f( 0.0 ) ;
f( 'A' ) ;
f( "hello" ) ;
}
利用例としては、まず型名を書くのがわずらわしい場合に使える。
void f( std::list< std::vector< std::string > > & c )
{
// 面倒
std::for_each( c.begin(), c.end(),
[]( std::vector< std::string> & elem )
{ ... }
) ;
// 簡単
std::for_each( c.begin(), c.end(),
[]( auto & elem )
{ ... }
) ;
}
また、コンパイル時に複数の型の実引数が渡される場合にも使える。
template < typename Printer >
void f( Printer && p )
{
p( 123 ) ;
p( 1.23 ) ;
p( "123" ) ;
}
int main()
{
f( []( auto && elem ){ std::cout << elem ; } ) ;
}
サイズ付き解放関数
解放関数(operator delete/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 )
{
// 何らかの方法でsizeバイトのストレージ確保
return ...
}
void operator delete( void * ptr , std::size_t size )
{
// ptrは解放すべきストレージのアドレス
// sizeはストレージのサイズ
}
ストレージ管理のデータ構造やアルゴリズムによっては、解放すべきストレージのサイズが分かっている場合、効率よく解放できるが、わからない場合、アドレスによって検索しなければならないということがある。生のストレージのサイズはC++の実装が管理しているので、C++14ではそのサイズを得る方法を追加した。
従来、クラスのメンバーとしての確保解放関数には提供されていた方法を、グローバルな解放関数のオーバーロードにも追加したものだ。
constexpr関数の制限緩和
C++11のconstexpr関数には、極めて厳しい制限があった。
- 関数の本体は実質return文のみ
- ifやswitchは禁止
- for, while, do-whileは禁止
- オブジェクトの変更は禁止
しかし、多くの数値計算は、条件分岐やループや値の変更が必要となる。
例えば、平方根を計算したいとする。数値Sの平方根Xは以下の方法で計算できる。
- 適当な初期値X0を取る
- Xn+1をXnとS/Xnの平均とする
- 十分な精度が得られるまでステップ2を繰り返す
通常の関数で実装すると、以下のようになる。
template < typename T >
T sqrt( T s )
{
T x = s / 2.0 ; // 適当な初期値
T prev = 0.0 ;
while ( x != prev )
{ // 十分な精度が得られるまでステップ2を繰り返す
prev = x ;
x = (x + s / x ) / 2.0 ; // ステップ2
}
return x ;
}
このコードには、条件分岐、ループ、変数の変更が出てくる。
C++11のconstexpr関数でこの処理を書くことはできる。ただし、条件分岐は条件演算子に、ループは再帰に、変数の変更は引数に書き換えなければならない。
template < typename T >
constexpr T sqrt_aux( T s, T x, T prev )
{
return x != prev ?
sqrt_aux( s, ( x + s / x ) / 2.0, x ) : x ;
}
template < typename T >
constexpr T sqrt( T s )
{ return sqrt_aux( s, s/2.0, s ) ; }
その結果、上のような極めて読みにくいコードを書かなければならない。
C++14では、この制限を大幅に緩和した。C++14のconstexpr関数では、以下のことができる。
- static/thread_localではなく、未初期化でもない変数の宣言
- if文とswitch文
- for, range-based for, while, do-while
- 定数式の評価時に寿命が始まるオブジェクトの変更
その結果、constexpr関数によるsqrtは、以下のように書ける。
template < typename T >
constexpr T sqrt( T s )
{
T x = s / 2.0 ; // 適当な初期値
T prev = 0.0 ;
while ( x != prev )
{ // 十分な精度が得られるまでステップ2を繰り返す
prev = x ;
x = (x + s / x ) / 2.0 ; // ステップ2
}
return x ;
}
通常の実行時関数のコードと、ほぼ変わりがなくなる。
constexprメンバー関数の暗黙のconst廃止
C++11では、constexprな非staticメンバー関数は、暗黙にconst修飾されていた。
struct X
{
// fは暗黙にconst
constexpr bool f() /* const */
{ return true ; }
} ;
C++14では、constexpr非staticメンバー関数は、暗黙にconst修飾されることはなくなった。
struct X
{
// 暗黙にconst修飾されない
constexpr bool f()
{ return true ; }
} ;
このような変更が行われた理由は、あるリテラル型のクラスをコンパイル時にも実行時にも使いたい場合、暗黙のconst修飾が問題になるからだ。
struct A
{
int data ;
// コンパイル時用getter
// 暗黙にconst修飾される
// 戻り値の型もconst修飾する必要がある
constexpr int const & get() const { return data ; }
// 実行時用getter
int & get() { return data ; }
} ;
int main()
{
// エラー
// オーバーロード解決は実行時getterを選択する
constexpr int compile_time_data = A{}.get() ;
A a ;
int runtime_data = a.get() ;
}
非staticメンバー関数のconst修飾はthisポインターを修飾する。したがって、非staticデータメンバーへの非constなリファレンスを返すには、const_castを使う必要がある。
struct A
{
int data ;
constexpr int & get() const
{
return const_cast<A *>(this)->data ;
}
} ;
C++14では、constexpr非staticメンバー関数の暗黙のconstがなくなったので、コンパイル時にも実行時にも使えるgetterをconstexpr関数で定義できる。
struct A
{
int data ;
constexpr int & get()
{
return data ; // OK
}
} ;
decltype(auto)
C++14では、型指定子にdecltype(auto)がというプレイスホルダー型が追加された。これは同じくプレイスホルダー型のautoに似ている。
auto a = 0 ; // int
decltype(auto) b = 0 ; // int
decltype(auto)は、あたかもdecltype(auto)のautoを、初期化子の中の式で置換したかのように振る舞う。
int & f()
{
static int obj ;
return obj ;
}
int main()
{
// int
auto a = f() ;
// int &
// decltype(f()) b = f() ;と同じ
decltype(auto) b = f() ;
}
auto指定子はテンプレートの実引数推定の使って型を推定する。
例えば、
auto x = expr ;
という宣言の場合、xの型Tは
template < typename T >
void f( T x ) ;
という関数テンプレートの実引数初期化子の式exprを渡した場合のT型になる。
テンプレートの実引数推定は、リファレンスが取り除かれたり、配列がポインターになったりと、様々な標準型変換が行われる。そのため、auto指定子による型は、初期化子の型と同一にはならないことがある。
一方、decltype(auto)は、あたかもdecltype(auto)のauto部分を初期化子の式で置き換えたかのように振る舞う。
例えば、
decltype(auto) x = expr ;
という宣言は、以下のように書いたものと同じになる。
decltype(expr) x = expr ;
decltype(auto)と書かずに、decltype(expr)と書いてもよいのだが、その場合、decltypeの中の式と、初期化子とで、式が重複する。
decltype( a + b * c - d ) x = a + b * c - d ;
もし式を間違えてしまった場合、結果も違ってしまう。後で式を書き変える場合には、二箇所を書き換えなければならない。
このようなコードの重複を避けるためと、後述する関数の戻り値の型推定のために、decltype(auto)が追加された。
戻り値の型推定
C++14では、通常の関数にも、lambda式のような戻り値の型推定が追加された。
戻り値の型推定とは、関数の戻り値の型を、return文のオペランドの式から推定させる機能である。
文法は、戻り値の型を記述するところに、auto、あるいはdecltype(auto)を書く。
// 前置記法
auto a() { return 0 ; }
decltype(auto) b() { return 0 ; }
// 後置記法
auto c() -> auto { return 0 ; }
auto d() -> decltype(auto) { return 0 ; }
上記の関数の型は、すべてint()となる。
戻り値の型推定は、return文のオペランドの式から、戻り値の型を推定してくれる。型推定の方法は、auto指定子 やdecltype(auto)指定子と同じ方法で行われる。
// int ( int & )
auto f( int & ref ) { return ref ; }
// int & ( int & )
decltype(auto) g( int & ref ) { return ref ; }
decltype(auto)がC++14に追加された理由は、戻り値の型推定のためだ。autoはリファレンスを型推定できない。auto &&は常にリファレンスになってしまう。
// int ( int & )
auto a( int & ref ) { return ref ; }
// int & ( int & )
auto && b( int & ref ) { return ref ; }
// int && ( )
auto && c() { return 0 ; }
decltype(auto)を使えば、リファレンス型はリファレンス型となり、リファレンスではない型はリファレンスではなくなる。
// OK
// ただし、int & ( int && )
auto && a( int && ref ) { return ref ; }
// OK、int && ( int && )
// ただしstd::moveを忘れると悲惨なことになる
auto && b( int && ref ) { return std::move(ref) ; }
// エラーになってくれる
// 戻り値の型は int &&だが、refはlvalue
decltype(auto) c( int && ref ) { return ref ; }
// OK
// std::move(ref)はrvalue
decltype(auto) g( int && ref )
{ return std::move(ref) ; }
戻り値の型推定を行う関数の本体には、複数のreturn文を書くことができる。ただし、return文のオペランドの型はすべて同一でなければならない。
// どのreturn文のオペランドも同じ型
auto f( bool b )
{
if ( b )
return 0 ; // OK、int
else
return 0 ; // OK、int
}
// return文のオペランドの型が違う
auto g( bool b )
{
if ( b )
return 0 ; // エラー、int
else
return 0L ; // エラー、long
}
戻り値の型推定は、再帰もできる。すべてのreturn文のオペランドの型が同じであればよい。
auto ackermann( unsigned m, unsigned n )
{
if ( m == 0u )
return n + 1u ;
else if ( n == 0u )
return ackermann( m - 1u, 1u ) ;
else
return ackermann( m - 1u,
ackermann( m, n - 1u ) ) ;
}
初期化リストを型推定することはできない。
auto f()
{
// エラー、std::initializer_list<int>
return { 0 } ;
}
virtual関数の戻り値の型推定を行うことはできない。
struct X
{
// エラー、virtual関数
virtual auto f() { return 0 ; }
} ;
virtual関数をサポートする技術的な問題はないのだが、C++14ではvirtual関数のサポートは見送られた。
戻り値の型推定を使った関数の再宣言はできる。ただし、型推定を行わない宣言になるので、宣言が一致していなければならない。
auto f() ; // OK、宣言
// OK、定義
auto f() { return 0 ; }
auto f() ; // OK、再宣言
// 別の関数
// エラー、戻り値の型の違いだけでオーバーロードはできない
int f() ;
関数fの戻り値の型はintだが、int型を明示的に記述した宣言は、別の関数の宣言になる。
戻り値の型推定を使った宣言は、定義なしで使うことはできない。
auto f() ; // 宣言
auto x = f() ; // エラー
// 定義
auto f() { return 0 ; }
auto y = f() ; // OK
[[deprecated]]
[[deprecated]]属性は、名前やエンティティを、「利用は非推奨」という意味付けをするための機能だ。
deprecated属性を付けられた名前やエンティティは、依然として使用できる。C++は実装は、deprecated属性が付けられた名前やエンティティの使用に対し、実装依存の挙動、例えば警告メッセージを発することができる。
// 利用は非推奨の関数
[[deprecated]] void do_something() ;
int main()
{
// 実装は何らかの方法で警告メッセージを出せる
do_something() ;
}
deprecated属性には、追加の文字列リテラルを記述できる。これは、例えばコンパイラーは警告メッセージとして出力したりできる。
[[deprecated("Use do_something_better() instead.")]]
void do_something() ;
deprecated属性が指定できる名前は、クラス、typedef名、変数、非staticデータメンバー、関数、名前空間、enum、列挙子、テンプレートの特殊化だ。
それぞれの属性を記述する文法は以下の通り。
// クラス
// class/structの後、識別子の前
class [[deprecated]] X { } ;
// typedef名
// 単純宣言の場合、先頭
[[deprecated]] typedef int t1 ;
// エイリアス宣言の場合、識別子の後
using t2 [[deprecated]] = int ;
// 変数
// 単純宣言の場合、先頭
[[deprecated]] int x ;
// 非staticデータメンバー
// メンバー宣言の場合、先頭
// staticデータメンバーは変数に含まれる
struct Y
{
[[deprecated]] int member ;
} ;
// 関数
// 先頭
[[deprecated]] void f() ;
// 名前空間
// namespaceの後、識別子の前
namespace [[deprecated]] ns { }
// enum
// enumの後、識別子の前
enum [[deprecated]] E { } ;
// 列挙子
// 識別子の後、初期化子の前
enum { enumerator [[deprecated]] = 0 } ;
// テンプレートの特殊化
// テンプレート化する宣言に同じ
template < typename T >
class [[deprecated]] TC { } ;
C++14規格の文面では、[[deprecated]]は、テンプレートの特殊化に対して指定される。テンプレート名自体には指定されない。
template < typename T >
class [[deprecated]] A { } ;
template <
template < typename > class T >
class B { } ;
// deprecated
A<int> a ;
// deprecatedではない
B<A> b ;
deprecated属性なしで宣言された名前やエンティティを再宣言する際に、deprecated属性をつけることができる。その逆もできる。
[[deprecated]] void f() ;
void f() ; // OK
void g() ;
[[deprecated]] void g() ; // OK
ただし、deprecated属性付きの宣言のあとにdeprecated属性なしの再宣言をしたとしても、deprecated属性は取り消されない。
deprecated属性の文字列リテラルの有無も、宣言ごとに変えてよい。
[[deprecated]] void f() ;
[[deprecated("DO NOT USE")]] void f() ; // OK
[[deprecated("obsolete")]] void f() ; // OK
メンバー初期化子とアグリゲート初期化の併用
C++11ではメンバー初期化子とアグリゲート初期化を併用できなかった。
// C++11
struct X
{
int a ;
int b = 2 ;
} ;
X x = { 1 } ;
// x.a == 1
// x.b == 0
アグリゲート初期化子では、対応する初期化子が存在しない非staticデータメンバーは、空の初期化リストで初期化されたものと同じ扱いになる。int型の場合、ゼロ初期化される。
C++14では、アグリゲート初期化で非staticデータメンバーに対応する初期化子がなく、かつ、メンバー初期化子がある場合は、メンバー初期化子が使われるようになった。
// C++14
struct X
{
int a ;
int b = 2 ;
} ;
X x = { 1 } ;
// x.a == 1
// x.b == 2
したがって、C++11とC++14では、メンバー初期化子とアグリゲート初期化を併用した場合、同じコードでもプログラムの意味が違うことになる。
変数テンプレート(variable template)
変数テンプレート(variable template)とは、変数宣言をテンプレート宣言する機能である。
template < typename T >
T value { } ;
int main()
{
value<int> ; // 0
value<int> = 1 ;
value<int> ; // 1
value<double> ; // 0.0
value<double> = 1.0 ;
value<double> ; // 1.0
}
変数テンプレートはテンプレートなので、明示的特殊化や、部分的特殊化もできる。
template < typename T >
constexpr bool value = true ;
// 明示的特殊化
template < >
constexpr bool value<long> = false ;
// 部分的特殊化
template < typename T >
constexpr bool value< T * > = false ;
int main()
{
value<int> ; // true
value<long> ; // false
value< int * > ; // false
}
変数テンプレートの用途としては、定数に型引数付きの名前を付けるのに使える。
たとえば、円周率定数に名前を付けたいとする。しかし、数値の型は様々ある。
constexpr int pi_i = 3 ;
constexpr double pi_d = 3.141592 ;
constexpr float pi_f= 3.14f ;
// 独自の無限精度整数クラス
const bigint pi_bigint("3.1415926535") ;
template < typename T >
T area_of_circle( T radius )
{
// 正しい型が分からない
return radius * radius * pi_??? ;
}
このように型によって名前を変えるのは面倒だ。
関数テンプレートを使うことはできるが、
// 汎用的な定数
template < typename T >
constexpr T pi() { return static_cast<T>(3.141592) ; }
// 特殊な型には明示的特殊化で対応
template < >
bigint pi<bigint>() { return bigint("3.1415926535") ; }
template < typename T >
T area_of_circle( T radius )
{
return radius * radius * pi<T>() ;
}
piは定数であり、実引数として渡すものは何もないのに、冗長な関数呼び出し式()を書かなければならない。
変数テンプレートがあれば、冗長な関数呼び出し式の文法を省くことができる。
template < typename T >
constexpr T pi = static_cast<T>(3.141592) ;
template < >
bigint pi<bigint>("3.1415926535") ;
template < typename T >
T area_of_circle( T radius )
{
return radius * radius * pi<T> ;
}
もうひとつの使い方としては、traitsをラップするのに使える。
#include <type_traits>
template < typename T >
constexpr bool is_pointer_v = std::is_same<T>::value ;
template < typename T >
void f( T )
{
// 面倒
bool a = std::is_pointer<T>::value
// 簡単
bool b = is_pointer_v<T> ;
}
traitsの戻り値を得るには、constなstaticデータメンバーである::valueを参照しなければならないが、変数テンプレートでラップすることにより、冗長な::valueを書かずにすむ。
本書を執筆している時点で、このように既存の<type_traits>を変数テンプレートでラップする標準ライブラリが、Library TSに提案されている。将来的には標準ライブラリに採用される見込みだ。