2014-01-pre-Issaquah mailingのレビュー: N3872-N3879
N3872: A Primer on Scheduling Fork-Join Parallelism with Work Stealing
これは提案ではなく、Fork-Joinにおける実装戦略の入門書的な論文。
Fork-Joinというのは、処理の単位を発生させて、ある地点で、発生させた処理が全て終わるまで待つものだ。Clik風の文法で書くと、以下のようになる。
// Clik風文法によるFork-Join
// オリジナルのスレッドが実行開始
e() ; // オリジナルのスレッドが逐次実行
spawn f() ; // 並行して実行される処理を生成
g() ; // fとは並行して実行される
sync ; // f, gの処理が両方終わるまで待つ
h() ; // 逐次実行
さて、ここで疑問が二つある。
- fとgを実行するスレッドは何か?
- hを実行するスレッドは何か?
fとgを実行するスレッドは何か?
Fork-Joinという概念による並列実行のスケジューラーの実装方法としては、Work-stealingというものがある。これは、MIT Cilk, Intel Clik Plus, Microsoft PPL, OpenMPで使われている。work stealingの概要としては、まずスレッドが処理を作る。スレッドが処理の実行を終えたら、そのスレッドは泥棒(thief)となり、別のスレッドで未実行のままたまっている処理を横取りする。
上の例では、spawn f() ; で処理が作成されている。さて、その処理を作成したオリジナルのスレッドは、fとgのどちらの実行をすればいいのだろうか。それには、ふたつの戦略がある。
- Child Stealing: fは泥棒スレッドに盗まれるようにし、オリジナルのスレッドはgを実行する
- Continuation Stealing: オリジナルのスレッドがfを実行する。オリジナルのスレッドの実行の継続は、別の泥棒スレッドに任せる。
これは、一見すると、些細な違いのように見える。しかし、ループの中で処理を延々と生成していくようなコードでは、この違いが問題になってくる。例えば以下のコード、
// 戦略の違いが問題となるコード
for ( int i = 0 ; i < n ; ++i )
{ spawn f( i ) ; }
sync ;
さて、このコードを実行すると、f(0)から、f(n-1)までの処理が生成される。ここで先ほどの二つの戦略を当てはめてみると。
Child Stealingでは、spawn f( i )を実行したあと、オリジナルのスレッドは、未実行の処理の数にむとんちゃくでfor文の実行を続けるので、f(i)が爆発的に生成されてしまう。これにより、未実行の処理を記録しておくリソースなどが浪費される。
Continuation Stealingでは、オリジナルのスレッドが、まずf(0)を実行しに行く。for文の実行は、泥棒スレッドに任せる。この戦略では、未実行の処理が爆発的にたまることはない。泥棒スレッドがあるだけ効率的に処理を生成し、実行できる。ただし問題は、本来実行していたスレッドとは別のスレッドが実行を継続することになるので、たとえばレジスターやTLSなど、退避、復帰させなければならないデータがたくさんあり、コストがかかる。
また、Child Stealingは、純粋にライブラリだけでも実装できる。Continuation Stealingは、コンパイラーのサポートが必要だ。
それに、Continuation Stealingは、一見逐次実行に見えるのに、実行するスレッドが変わるので、初心者を混乱させる。
hを実行するスレッドは何か?
さて、syncによってjoinしたあと、hを実行するスレッドは、いったいなんだろうか。これには二つの実装戦略がある。
- Stalling: オリジナルのスレッドが実行する。
- Greedy: 最後に処理を終えたスレッドが実行する
Stallingの、オリジナルのスレッドが実行を再開するのは、とてもわかりやすい。ただし、それはオリジナルのスレッドが、すべての処理が終わるまで待たなければならない。そのためにStallingと呼ばれている。うかつに時間のかかる処理を、オリジナルのスレッドが盗んでしまうと厄介だ。
Greedyは、最後に処理を終えたスレッドが実行するので、スレッドが何もしないで待つ必要がない。ただし、オリジナルのスレッドとは別のスレッドが実行を継続する可能性があるので、TLSなどがうまく動かないし、TLSもうまく動かすようにしようとすれば、相当のスレッド環境の退避、復帰のコストがかかる。それに、わかりにくい。
TBBとPPLはStallingをサポートしている。Cilkは、Greedyをサポートしているが、Stallingをサポートする実験的なライブラリもある。OpenMPは、なんとまた、どちらもサポートしている。
[汚らしいド素人の書いたHTML] N3873: Improved insertion interface for std::{unordered_,}map
論文のタイトルは"Improved insertion interface for unique-key maps", 論文のHTMLのtitle要素は、"Improved insertion interface for std::{unordered_,}map"という一貫性に欠けるマークアップ。しかも、物理的な改行が意味を持つコードをマークアップするのに、既存の意味的にもふさわしいpre要素やcode要素を使わずに、div.code { white-space : pre-wrap ; }しているという、汚らしいド素人が書いたであろうマークアップの論文。
mapとunordered_mapにemplaceのようなものを追加する提案。
既存のmapとunordered_mapのemplaceは、キーが存在する場合、要素の挿入を行わない。問題は、要素の挿入が行われなかったとしても、依然として、実引数で渡したオブジェクトは、ムーブ後の状態になる可能性がある。
// emplaceの例
std::map<std::string, std::unique_ptr<Foo>> m ;
m["foo"] ; // キー"foo"を挿入
std::unique_ptr<Foo> p(new Foo) ;
// キーはすでに存在する
// pはムーブ後の状態になっている可能性がある
auto res = m.emplace("foo", std::move(p)) ;
挿入を行わないのならば、ムーブしなくてもいいし、実際、そのように実装することもできる。ただし、規格は上の例で、pをムーブ後の状態にすることを禁止していないし、実際にムーブ後の状態にするライブラリ実装もある。なぜかというと、emplaceは、実引数から、std::pair< std::string, std::unique_ptr<Foo> >のオブジェクトを生成するのだが、この生成は、キーの検索の前に行われることが、規格上許されているからだ。規格上許されている以上、そのように実装しても何の問題もない。
そこで、キーが存在した場合は、実引数のオブジェクトに何の変更をもくわえないことを保証した、新しいメンバー関数、emplace_stableを追加する提案。
// emplace_stableの例
std::map<std::string, std::unique_ptr<Foo>> m ;
m["foo"] ; // キー"foo"を挿入
std::unique_ptr<Foo> p(new Foo) ;
// キーはすでに存在する
// pはムーブ後の状態になることはないことが保証されている
auto res = m.emplace_stable("foo", std::move(p)) ;
なぜ、既存のemplaceをemplace_stableの挙動にしないのかというと、どうも、うまい文面案が思いつかないらしい。なんともプラグイン的な解決方法だ。
提案では、もうひとつ、empalce_or_updateというメンバーを付け加える。これは、キーが存在する場合でも、上書きムーブする。
std::map<std::string, std::string> m ;
m["foo"] = "bar" ;
std::string value{ "hoge" } ;
// キーが存在するので上書きされることはない
m.emplace_stable( "foo", value ) ;
m["foo"] ; // "bar"
// キーが存在するが上書きする
m.emplace_or_update("foo", std::move(p)) ;
m["foo"] ; // "hoge"
emplace_or_updateは、emplace_stableがあれば、効率を落とさず簡単に実装できる。なぜならば、emplace_stableは、イテレーターと、挿入を行ったかどうかをpairで返すからだ。
auto it = m.emplace_stable("foo", std::move(p));
if (!it.second)
{
it.first = std::move(p);
}
とはいえ、こんなコードをいちいち書きたくない。標準ライブラリで提供されているべきだ。
[クソみたいなPDF] N3874: Light-Weight Execution Agents
スレッドとかタスクとかSIMDとかベクトル実行とか、様々な名称で呼ばれている、light-Weight Execution Agent(軽量実行物)の定義を与える提案。
Execution Agentとは、実行の単位の名称である。
C++11では、スレッドを導入し、C++に並列実行の道具を与えた。
Execution Agent(実行物)とは、すでに規格で定義されている用語(§30.2.5.1 paragraph 1)だ。スレッドは、「ブロックされていない限り、いずれ実行が進む」("should eventually make progress if they are not blocked.")と定義されている。これは、とても強い保証を持っている。
ところで、C++11で入ったスレッドというのは、たとえばPOSIXのpthreadをモデル化したものであり、多くの実装ではOSの提供するスレッドである。このようなスレッドは、とても重たい。しかし、そのような重たいものは、大量に生成すると、リソース使用量の点でコストがかかる。軽い並列処理には、もっと軽い単位のExecution Agentが使われるべきである。すでに、そのような提案はあるが、みな用語がバラバラである。その根底にある基本的なExecution Agentという概念を、共通化してまとめて定義しよう、と、こういう提案である。
まず、Foward progress(実行が進む)保証について考察する。これにはいくつかの保証の強弱が考えられる。
- Concurrent execution
- スレッドのような、いずれ実行が進むという強い保証
- Parallel execution
- スレッドプールやFork-joinのような、弱い保証。クリティカルセクションの使用にも難あり。
- SIMD execution
- SIMD演算のモデル化。これはやや特殊な保証。
- Parallel+SIMD execution
- ParallelとSIMDを組み合わせて、両方の良いとこどりをねらった保証。ただし、この保証の存在理由には疑問もある。
また、スレッドの紐付けられた状態という点からも考察できる。たとえば、TLSや、this_thread::get_id()や、ロック所有するスレッドという点だ。
論文は最後に、いくつかの疑問を提示している。
[おぞましいPDF] N3875: Run-time bound array data members
実行時にサイズを指定できる配列をクラスのデータメンバーとして持つことができるようにする提案。
ただし、Cとはだいぶ違う方向だ。そして、名前も、実行時サイズ配列(runtime sized array)ではない。
まず、文法をひと通り見ていくことにしよう。
// 文法例
struct runtime_size_bound_class
{
char [] runtime_bound_array ;
// array constructor
runtime_size_bound_class( std::size_t size )
: runtime_bound_array[ size ]
{
// ...
}
} ;
他にも色々と文法案が提案されているが、どれもこれも一様に気持ち悪い。
まず、データメンバーの宣言文法から見ていこう。これは、実行時束縛配列(runtime bound array)と呼ばれていて、以下のように宣言する
type [] name ;
なぜこうならなければならないのか。当初、runtime bound arrayの文法は、通常のtype name [] ; になる予定だったのだが、それだと、既存のCの構造体のテクニックとかぶってしまうのだ。
// 非常によく使われているテクニック
struct X
{
std::size_t size ;
char buf [] ;
} ;
int main()
{
constexpr std::size_t size = 100 ;
X * ptr = reinterpret_cast< X * >( std::malloc( sizeof(X) + size ) ) ;
ptr->size = size ;
}
そのため、文法を変える必要があったのだ。
実行時束縛配列のデータメンバーは、複数持つこともできる。
// 複数個持つ例
struct X
{
char [] a ;
char [] b ;
X( ) : a[ 1 ], b[ 2 ]
{ }
} ;
さて、runtime bound arrayのサイズは、array constructor(配列コンストラクター)と呼ばれるコンストラクターで指定する。
runtime_size_bound_class( std::size_t size )
: runtime_bound_array[ size ]
array constructorは、メンバー初期化子の文法を拡張して[]でサイズを実行時に指定する。
array constrcutorは、必ず、クラス定義の中で定義しなければならない。
// 間違いの例
struct X
{
char [] a ;
X() ;
} ;
// ill-formed
X::X() : a[1] { }
初期化も、通常通りだ
// 初期化の例
X() :
a[1], // デフォルト初期化
b[1](), // 値初期化
c[1]{} // リスト初期化
{ }
さて、runtime bound arrayをデータメンバーに持つクラスは、runtime size bound class(実行時サイズ束縛クラス)と呼ばれる。runtime size bound classをデータメンバーに持つクラス、runtime size bound classから派生するクラスも、runtime size bound classとなる。
runtime size bound classのオブジェクトは、自動変数として使うことしかできない。つまり、一切の動的に確保する操作は禁止されている。たとえばnewできないし、std::vectorのvalue_typeとなることもできない。
// 禁止例
// ill-formed
new runtime_size_bound_class ;
// ill-formed
std::vector< runtime_size_bound_class > v ;
runtime size bound classへのsizeofは禁止されている。
// sizeofは許可しないィィィッ!
struct X
{
char [] a ;
X() a[1] { }
} ;
// ill-formed
std::size_t size = sizeof( X ) ;
なぜdynarrayではダメなのかというと、dynarrayで自動ストレージを実行時サイズ確保しようとすると、実装にコンパイラーマジックが必要になるからだ。C++は歴史的にコア言語とライブラリを分割している言語である。C++11では、std::initializer_listという例外が追加されたが、あれは例外中の例外で、基本的には、コア言語とライブラリは直接関わらない。
そのため、ライブラリを実装できる機能は、コア言語で提供されているべきである。そのような機能が、実行時束縛配列というわけだ。実行時束縛配列を使えば、dynarrayが実装できる。
さて、論文では、この提案の他にも、様々な提案をしている。
非inlineコンストラクター
コンストラクター定義をクラス定義の外に書きたい。しかし、配列のサイズの指定方法は、実装の都合上、クラス定義の中になければならない。そこで、メンバー初期化子だけ、inline definition(インライン定義)という名前で、クラス宣言に書くことができる案も、論文に書かれている。
// 提案
struct X
{
char [] a ;
// インライン定義
X() : a[1] { }
} ;
// 本物の定義
X::X() // メンバー初期化子はすでに指定されているので書けない
{
// ...
}
多次元配列
現在の提案では、一次元配列しかサポートしていないが、文法を多次元に拡張することもできる。
// 多次元実行時束縛配列
struct X
{
char [][] a ;
X( std::size_t x, std::size_t y )
: a[x][y]{}
{ }
} ;
sized constructor
サイズコンストラクターといって、コンストラクターをクラス定義の外に書く文法の別の提案。これはDaveed Vandevoordeの提案。
// sized constructorの例
struct X
{
char [] a ;
int x ;
X( std::size_t size, int x ) sizeof( a[size] ) ;
} ;
X::X( std::size_t size, int x ) : x(x)
{
// ...
}
相変わらずだが、Daveed Vandevoordeの提案する文法は、毎回キモい。彼はEDGという不自由なC++コンパイラーフロントエンドの開発者であるので、とりあえず非曖昧でパースしやすい文法を再優先で考案するのだろう。
[PDF早く滅んでくれ] N3876: Convenience Functions to Combine Hash Values
unordered associative container(unordered_map, unordered_multimap, unordered_set, unordered_multiset)は、一般にはハッシュとよばれる仕組みで要素を管理するコンテナーである。具体的には、value_typeから短時間で計算できる短い情報量のハッシュ値を計算して、ハッシュ値同士の比較を行うことで、定数時間の複雑性を可能にしている。
問題は、value_typeからハッシュ値を計算する方法が必要だということだ。標準ライブラリは、基本型やstd::stringなどの標準ライブラリには、ハッシュ値の計算方法を提供している。ただし、ユーザー定義型については、利用者がクラステンプレートstd::hashを特殊化するか、あるいはクラスで実装してテンプレート実引数で渡すなどして、ハッシュ値の計算方法を実装しなければならない。
適切なハッシュ値の計算というのは、とても難しい問題である。平均的なプログラマーがたやすく扱える問題ではない。
たとえば、以下のクラスを考える。
// 顧客クラス
class Customer {
public:
// ...
std::string getFirstname() const;
std::string getLastname() const;
int getAge() const;
} ;
bool operator== (const Customer&, const Customer&);
このようなクラスのオブジェクトのハッシュ値を計算したい。ところで、都合の良いことに、std::stringやintといった型は、すでに標準ライブラリによって、ハッシュ値を計算する実装が提供されている。これを使って、以下のように実装してはどうか。
// 非常に問題のあるコード例
class CustomerHash
{
public:
std::size_t operator() (const Customer& c) const
{
return std::hash<std::string>()(c.getFirstname()) +
std::hash<std::string>()(c.getLastname()) +
std::hash<std::string>()(c.getAge()) ;
}
} ;
std::unordered_set<Customer,CustomerHash> coll;
このコードにより生成されるハッシュ値は、極めて質が悪くなる。ハッシュ値は、単に足しあわせてはいけないのだ。
ではどうすればいいのか。筆者はこのレビューを書くにあたって、Donald Knuth先生の大著、The Art of Computer Programming Volume 3を読んだが、やはり、適切なハッシュ値の計算を書くのは難しい。コンテナーという利用目的にあった効率的なハッシュ値を計算しなければならない。ハッシュ値の計算は高速に行われるべきだが、生成されるハッシュ値の質が低くては元も子もない。
そこで、複数のハッシュ値から、まあまあ使える程度のハッシュ値を生成するライブラリを、標準で入れてはどうか。
たとえば、Boostには、そのようなライブラリが存在する。
Combining hash values - 1.35.0
Function template hash_combine - 1.35.0
hash_combineは、たとえば、以下のように実装できる。
// hash_combineの実装例
template <typename T>
void hash_combine (std::size_t& seed, const T& val)
{
seed ^= std::hash<T>()(val) + 0x9e3779b9
+ (seed<<6) + (seed>>2);
}
これは、最高のハッシュ値の計算方法ではないが、単なる足し算よりはいくらかマシな、複数のハッシュ値のハッシュ値の計算方法だ。
議論のしどころはたくさんある。たとえば、seedを直接公開しないようにして、もっといい実装ができる余地を残すなどだ。
[冗長すぎるPDF] N3877: Centralized Defensive-Programming Support for Narrow Contracts (Revision 3)
防衛的プログラミングをサポートするための標準ライブラリ。Bloombergが出したこの論文は、その格調高さに反比例して、中身は薄すぎる。文章量だけは無駄に常用で長いが、本質が薄っぺらい。
要するに、中身はCプリプロセッサーによるassertマクロに毛が生えたようなものだ。assertに引っかかった時の挙動を指定できたり、例外が投げられたりといった機能があるだけのassertだ。
筆者はCプリプロセッサーを使ういかなる機能にも反対しており、この提案にも当然ながら反対する。
[あまりにも冗長なPDF] N3878: Extensions to the Concept Introduction Syntax in Concepts Lite
この提案には、まだ文面案がないので、このレビューの解釈は間違っている可能性がある。そもそも、文法が大幅に変更される可能性がある。
Concept Liteについては、もう十分に有名になったので、いまさら説明するまでもないだろう。Concept Liteは、C++11がまだC++0xと呼ばれていた時代に、一旦はドラフトに入りながら、標準化委員会内でconcept mapを暗黙に生成するべきか、明示的に記述させるべきかで意見が二分されたコンセプトの、テンプレート実引数のチェック部分だけを切り取った、軽量版のコンセプトだ。
たとえば、2種類のイテレーターFor1, For2をマージして、Outというイテレーターで出力可能かどうかを判定するconstexpr関数Mergeableの宣言が、以下のように書かれていたとする。
// Mergeable
template < typename For1, typename For2, typename Out>
constexpr bool Mergeable() ;
// 定義省略
さて、このconstexpr関数をConcept Liteに使いたい場合は、以下のように記述する。
// Concept Liteの例
template < typename For1, typename For2, typename Out >
requires Mergeable< For1, For2, Out >()
void merge( For1 p1, For1 q1, For2 p2, For2 q2, Out o ) ;
これは冗長で書くのが面倒だ。以下のように書けるようにしてはどうか、という提案。
// N3878提案
Mergeable{ For1, For2, Out }
void merge( For1 p, For1 q, For2 p2, For2 q2, Out o );
このように書けば、最初の長ったらしく書いたものと同じように扱われるようにする提案だ。
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3879.pdfもうすこし順序立てて解説する。まず、従来のunconstrained templateが、以下のようだとしよう。
// unconstrained template
template < typename C >
void function( C ) ;
このテンプレート仮引数Cに対して、UnaryConceptというconstexpr関数を適用して、constrained templateにしたいとする。
// constrained template
template < typename T >
constexpr bool UnaryConcept() ;
requires句を使うと、以下のように書ける。
// requires句を使う例
template < typename C >
requires UnaryConcept<C>() ;
void function(C) ;
これは面倒だ。そのため、今のConcept Liteの提案は、以下のようなシンタックスシュガーを提供している。
// 便利なシンタックスシュガー
template < UnaryConcept C >
void function(C) ;
class/typenameと書くべきところに、直接constexpr関数名を指定できるのだ。
それを延長して、以下のように書けるようにする。
// N3878提案
template < UnaryConcept { C } >
void function( C ) ;
そして、さらに突き進めて、以下のように書けるようにする(はず、あるいは論文が間違っているのか)
// N3878提案
UnaryConcept { C }
void function( C ) ;
テンプレートの冗長な文法が、だいぶ簡略化された。いや、簡略化し過ぎで、個人的には落ち着かない。しかし、プログラマーというものは、簡単な文法を好むものであるから、むしろこのくらい大胆に簡略化したほうがいいのかもしれない。
この提案されているシンタックスシュガー的な文法が、どのように変換されるのか。まず、以下のようなコード
// N3878提案
SomeConcept{ A, B, C }
void function( A, B, C ) ;
において、SomeConceptはconstexpr関数名である。braceで囲った識別子一つ一つについて、テンプレート仮引数として指定したことになる。
// シンタックスシュガー変換後
template < typename A, typename B, typename C >
requires SomeConcept< A, B, C >()
void function( A, B, C ) ;
もし、すでに宣言したテンプレート仮引数を使った場合は、ill-formedとなる。たとえば、以下のようなコード
// ill-formedな例
template < typename A, SomeConcept{ A, B } >
void function( A, B ) ;
は、以下のように展開されるので、
// 展開後
template < typename A, typename A, typename B >
requires SomeConcept{ A, B {
void function( A, B ) ;
ill-formedとなる。
ところで、Concept Liteでは、constrained関数テンプレートを宣言する新しい文法、terse notationを提案している。これは聡明な読者ならば、いまさら説明するまでもないであろうが、念の為に説明しておくと、以下のようなコードが、
// terse notationの例
void sort( Container & c ) ;
以下のように書いたものとみなされる。
// 上記のterse notationと同等のコード
template < Container __Contaiener >
void sort( __Container & c ) ;http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3879.pdf
もちろんこれは、以下のような意味だ。
// さらに展開後
template < typename __Container >
requires Container<__Container>()
void sort( __Container & c ) ;
そう、聡明な読者ならば、もう気がついているだろう。N3878提案の拡張は、このterse notationにも適用できる。
// terse notationのN3878拡張の例
void sort( Container{C} & c ) ;
これは、以下と等しい。
// 同等のコード
template < typename C >
requires Container<C>()
void sort( C & c ) ;
なぜ、これが役に立つのか。現在のterse notationは、同じコンセプトを満たす別の型を複数、仮引数に取りたい場合、使うことができない。
たとえば、以下のコードは、
void f( SomeConcept a, SomeConcept b ) ;
以下のコードと同じ意味である。
template < typename __SomeConcept >
requires SomeConcept<__SomeConcept>()
void f( __SomeConcept a, __SomeConcept b ) ;
したがって、もし、aとbが違う型で、どちらの型もSomeConceptを満たして欲しい場合は、terse notationは使えない。面倒でも従来のやり方で書かなければならない。
// 従来のやり方
template < SomeConcept A, SomeConcept B >
void f( A a, B b ) ;
N3878拡張を使えば、上記と同じ意味を、terse notationと組み合わせることにより、以下のように書くことができる。
// N3878提案
void f( SomeConcept{A} a, SomeConcept{B} b ) ;
また、N3878提案で宣言された名前は、宣言されたことになるので、例えばterse notationでも面倒な以下のようなコードが、
// 面倒なTerse Notationコード
void f( RandomAccessIterator first, RandomAccessIterator Last ) ;
以下のように書ける。
// N3878提案
void f( RandomAccessIterator{R} first, R last ) ;
N3878拡張は、新しいテンプレート仮引数を宣言するので、たとえば、以下のようなコードはill-formedとなる。
// ill-formed
void f( RandomAccessIterator{R} first, RandomAccessIterator{R} last ) ;
なぜならば、これは以下と同等の意味になってしまうからだ。
// ill-formed
template < RandomAccessIterator R, RandomAccessIterator R >
void f( R first, R last ) ;
また、現在のConcept Liteの提案では、変数テンプレートもconstrained templateにでき、また、変数宣言の型引数の代わりに使うことができるとしている。
// constrained変数テンプレート
RandomAccessIterator r = std::find( p, q, r ) ;
これは、読者のような本物のC++プログラマーには、いまさら説明するのは失礼に当たるかも知れないが、念の為に解説すると、以下のような意味である。
template < typename __RandomAccessIterator >
requires RandomAccessIterator<__RandomAccessIterator>()
__RandomAccessIterator r = std::find( p, q, r ) ;
一貫性を保つため、N3878拡張は、constrained変数テンプレートにも適用できる。
// N3878提案
RandomAccessIterator{R} r = std::find( p, q, r ) ;
これは、rの型名に名前をつけることができるという点で、素のterse notationより優れている。decltype(r)などと書きたくはない。
また、提案では、以下のような文法にも触れている。
// どこまで本気かわからないコード
auto{T} t = foo() ;
// Tはtの型
これは・・・いやはや
[明示的PDF警告] N3879: Explicit Flow Control: break label, goto case and explicit switch
これは面白い。
この提案は、break label, continue label(Javaと同等), goto case 定数式, goto default(C#と同等), explicit switch文(C#と同等)
break labelとcontinue labelは、ネストしたfor, while, do-whileから抜けるのに使うことができる。
// break labelの例
beginning_of_the_loop:
for ( auto && elem : v )
for ( auto && elem : w )
{
break beginning_of_the_loop ;
}
// continue labelの例
for( auto && elem : v )
{
middle_of_the_loop:
for ( auto && elem : w )
for ( auto && elem : x )
{
continue middile_of_the_loop ;
}
}
goto case 定数式は、switch文の中で、caseラベルを指定してgotoするのに使うことができる。
// goto case 定数式
switch( cond )
{
case 0 :
break ;
default :
goto case 1 ;
}
explicit switchは、caseラベルひとつひとつが、ブロックスコープを持つ。これにより、次のcaseラベルかdefaultラベルに到達した時点で、switch文を抜ける。もうbreakを書き忘れて、下まで突き抜けてしまうという、しょうもないが人間であるがゆえに起こりうるバグに悩まされることはない。また、わざわざブロック文を使わずとも、ブロックスコープになっているので、変数の宣言も楽になる。
// explicit switchの例
explicit switch( cond )
{
case 1:
// ...
case 2 :
// flag == 1の場合、ここは実行されない
}
連続するcaseラベルは、ひとつのブロックスコープにまとめられる。
explicit switch( cond )
{
case 1 :
case 2 :
// ブロックスコープ開始
// cond == 1 or 2の場合、ここが実行される
// ブロックスコープ終了
}
caseラベルがブロックスコープを持つということは、変数ももちろん、スコープに従う。
switch ( cond )
{
case 1 :
int x = 0 ;
case 2 :
int x = 0 ; // エラー、おなじスコープ
}
explicit switch ( cond )
{
case 1 :
// {
int x = 0 ;
// }
case 2 :
// {
int x = 0 ; // OK、別のスコープ
// }
}
ドワンゴ広告
この記事はドワンゴの勤務中に書かれた。
ドワンゴは本物のC++プログラマーを募集しています。
CC BY-ND 4.0: Creative Commons — Attribution-NoDerivatives 4.0 International — CC BY-ND 4.0