2014-01-pre-Issaquah mailingの簡易レビュー Part 1
2014-01-pre-Issaquah mailingが公開された。
今回は、論文の本数が多いのと、ドワンゴに雇われているので、本気でじっくりと論文を読んで解説しているし、ライブラリの論文も読み飛ばさずに読んでいるので、いつもより時間がかかる。そのため、いくつかのパートに分けて公開することにした。
今回はドラフトの更新はなし。
今回の新機能の提案の論文には、SG10のためのマクロ名の提案が目立つ。SG10というのは、Cプリプロセッサーによる機能テストのマクロ名を標準化しようという提案のStudy Groupだ。醜悪で将来廃止されるべきCプリプロセッサーに依存する機能をこれ以上増やさないで欲しいのだが。
std::array<T>を返すmake_arrayの提案。以下のように使う。
// make_arrayの利用例
std::array<int, 3> a = std::make_array( 1, 2, 3 ) ;
std::array<long, 2> b = std::make_array( 1, 2L ) ;
どの型になるかは、std::common_typeを使って決定される。ただし、narrowing conversionは禁止されている。
// narrowing conversionは違法になる
auto a = std::make_array( 1, 2U ) ; // エラー
明示的に型を指定することで、キャストされる。
// 明示的に型を指定する例
auto a = std::make_arrary<long>( 1, 2U ) ; // OK
もちろん、現実に書くときには、auto指定子を使うべきだ。
// make_arrayの利用例
auto a = std::make_array( 1, 2, 3 ) ;
auto b = std::make_array( 1, 2L ) ;
[PDFを廃止する議論が必要とされている] N3825: SG13 Graphics Discussion
C++14に標準ライブラリとして入れるために設計を進めている軽量グラフィックライブラリの土台として、何を使うかの議論のまとめ。
C++にグラフィックライブラリを入れたいが、グラフィックライブラリを一から設計するのは、C++標準化委員会の規模を大幅に超えているので、既存のグラフィックライブラリを土台として使い、必要であればC++風にバインドして使おうというのが、これまでの議論。では、その土台を何にするかというのが、この議論の主題だ。
土台には二つの意見があった。既存の規格を使うものと、既存のライブラリを使うものだ。
既存の規格というのは、例えばSVG+Canvasのような、標準規格を参照して、その設計を元にC++風にバインドして使うということだ。
既存のライブラリというのは、例えばcairoのような、すでに実装されているライブラリを元にして、必要であればC++風にバインドして使うということだ。
どうやら、議論の方向は既存のライブラリ、それもcairoを土台とするところに向かったらしい。cairoはCで書かれているが、オブジェクト指向であり、正しくconstを使っており、またドキュメントも非常に充実している。
cairoには、cairommというC++へのバインドがあるが、どうもこのcairommは、2010年から更新されていないので、おそらくプロジェクトとしては死んだのではないかと思われる。
そこで、cairo本体を土台とし、数ページ程度に収まるような機会的な変換ルールを定義して、cairoのインターフェースをC++風に合わせてはどうか。論文では、そのための変換ルール案も箇条書している。また、cairoのドキュメントなどがISO規格として使えるような著作権許諾が得られるかなどの調整も必要だ。
本当にC++14の標準ライブラリにグラフィックライブラリが入るのだろうか。
N3827: Working Draft Technical Specification - URI
URIライブラリのドラフト
[汚いPDF] N3829: apply() call a function with arguments from a tuple (V2)
tupleの要素をすべて関数の実引数に渡すライブラリー、applyの提案。
これは欲しい。自前で実装するのは簡単だが、面倒だ。
N3830: Scoped Resource - Generic RAII Wrapper for the Standard Library
汎用的なRAIIラッパーライブラリー、scoped_resourceの提案。
RAIIというのは、Resource Acquisition Is Initializationの略である。C++では、クラスオブジェクトにリソースを所有させ、デストラクターが動くタイミングで解放処理をさせるような一連の技法を言う。
// RAIIの例
class raii_holder
{
int * owned ;
public :
explicit raii_holder( int * ptr )
: owned( ptr ) { }
raii_holder( raii_holder const & ) = delete ;
raii_holder & operator =( raii_holder const & ) = delete ;
int * get() { return owned ; }
~raii_holder()
{
delete owned ;
}
} ;
int main()
{
raii_holder r( new int(0) ) ;
*r.get() = 123 ;
// 自動的にdeleteされる
}
ポインターの場合は、すでにunique_ptrがあるが、ポインター以外のリソースを管理するライブラリは、手で書くしかない。unique_ptrは、ポインター以外のリソース(単なる整数値で表現される環境依存のハンドルなど)を管理するのは面倒だ。
問題は、このようなRAIIラッパーを手でいちいち書くのは面倒だ。これは、テンプレートで汎用化すべき種類のコードだ。C++14までのコア言語機能を総動員し、最新の設計をすれば、そのような汎用RAIIラッパーライブラリが実現できる。
// 提案中のライブラリ
int main()
{
auto r = std::make_scoped_resource( &::operator delete, new int(0) ) ;
// 自動でdeleteされる。
}
まだ設計は変わるが、このライブラリは非常に柔軟である。例えば、解放のための処理が、単に引数をひとつとるものではない場合がある。たとえば、以下のような実装依存のリソースの確保、解放関数があったとする。
// System defined APIs:
void* VirtualAlloc(
void *pAddress,
size_t size,
unsigned int fAllocationType,
unsigned int fProtect);
bool VirtualFree(
void *pAddress,
size_t size,
unsigned int uiFreeType);
これはこのようになっているので、いまさらどうしようもない。提案中のscoped_resouceでは、これを単に、複数のリソースをまとめて管理するという形で扱う。わざわざユーザーがVirtualFreeと複数のリソースをラップする必要はない。
// VirtualFreeにもらくらく対応
int main()
{
auto r = std::make_scoped_resource( &VirtualFree,
VirtualAlloc( nullptr, PAGE_SIZE, MEM_COMMIT, PAGE_HARDWARE ),
0,
MEM_RELEASE ) ;
// 自動的にVirtualFree
}
Variadic Templatesを使えば、任意個のリソースを管理できる。
実は、管理するリソースは、0個でも良い。
// 0個のリソースを管理する例
int main()
{
auto r = std::make_scoped_resource(
[]{ std::cout << "I'm exception safe. Thank you very much." << '\n' ; }
) ;
// 自動的にlambda式が呼ばれる
}
これにより、オブジェクトrが破棄されるタイミングで処理を実行することもできる。
このようなライブラリを手で書くのは簡単だが、正しく書くのは面倒なので、標準ライブラリに入って欲しい。
この提案では、あくまでunique_ptrをポインター以外にも広げたscoped_resourceを提案している。参照カウンターを使ったものは提案されていない。そのようなライブラリも実装可能だが、この論文では、それは後から検討する話だとしている。追加は簡単なので、とりあえずはこれを議論したほうがいい。
[気分を害するPDF] N3831: Language Extensions for Vector level parallelism
Clik PlusやOpenMPで提供されている、プログラムにSIMD化のためのヒントを与える構文の提案。
SIMD化できるループは、極めて厳しい制約が課される。論文ではこれをcountable loopと呼んでいる。
SIMD loopをコア言語でサポートするために、for文に変更がくわえられる。
// SIMD loop
for simd ( int i = 0 ; i < 10 ; ++i )
{
// 処理
}
キーワードsimdは、文脈依存キーワードなので、この文脈でなければ予約されていない。そのため、自由に識別子として使うことができる。
for文の条件式につかえる式は、非常に限られている。以下の組み込みの演算子しか使えない。
// SIMD forの条件式で使える式
relational-expression < shift-expression
relational-expression > shift-expression
relational-expression <= shift-expression
relational-expression >= shift-expression
equality-expression != relational-expression
また、for文の3つ目のオペランドにも、前置後置のインクリメントとデクリメント、+=, -=, E1 = E2 + E3, E1 = E2 - E3ぐらいしか使えない。これは組み込みのコンマ演算子で区切ることができる。その中で使える識別子で参照されているものにも、強い制約がある。
return, break, goto, switchなどを使って、ループの中から外に脱出したり、また外からループの中に飛び込んだりすることはできない。
また、SIMD関数というものもある。これは、文脈依存キーワードsimdでマークされる関数のことである。
// SIMD関数の例
int f() simd { }
[]() simd { } ; // もちろんlambda式にも使える
さて、このようにsimdキーワードでマークされたループや関数には、複数のコードが生成され、実行時にふさわしいものが選ばれる。つまり、これはコンパイラーへのヒントである。
[理論的に美しくないPDF] N3832: Task Region
C++でstrict fork-joinを実現するためのライブラリ。
こうしてみると、並列処理というのは、理論が数十年ぐらい先行していて、いま、ようやく理論をC++のような実用的な言語で使うために、現実の場に落としこんできているというのがわかる。このライブラリはかなり理論よりの内容になっているが、はたしてここまでガチガチの机上の理論に裏打ちされたライブラリが、普通のプログラマーに簡単に使えるようなライブラリに落ち着くだろうかという懸念はある。
原理としては、task_regionに関数オブジェクトを渡して、その中で並列実行したいタスクはtask_runに関数オブジェクトを渡して実行するということになっている。そして、最後にすべての並列処理をjoinして集める。
ただしこれはかなり理論的なライブラリなのでかなり制約がなくす方向で進んでいる。たとえば、関数を呼び出した時のスレッドと関数が戻ってきた時のスレッドが異なることは、従来起こりえないことだったが、このライブラリでは、起こりえてしまう。これをどうしようかということが議論されている。また、まだ完全に処理が終わらない状態で関数が戻るということもあるので、その場合をどうしようかという議論もある。
[PDF不使用原則も提案したい] N3839: Proposing the Rule of Five, v2
従来の三原則に変えて、五原則を導入する提案。
三原則とは、コピーコンストラクター、コピー代入演算子、デストラクターの、どれかひとつをユーザー定義した場合、残りの二つも、おそらくはユーザー定義が必要になるであろうという原則だ。
C++11では、ムーブという概念が、直接コア言語に組み込まれた。当初、ムーブを直接コア言語でサポートするかどうかは、決まっていなかった。しかし、ムーブのような基本的な概念は、コア言語で認識したほうがいいだろうという結論になり、従来のコピーコンストラクター、コピー代入演算子にくわえて、ムーブコンストラクター、ムーブ代入演算子が、新たにコア言語で特別なメンバー関数として認識されるようになった。
もし、C++11で、ムーブコンストラクター、ムーブ代入演算子がユーザー定義されていた場合、コピーコンストラクター、コピー代入演算子は、暗黙にdefault化される。この挙動はdeprecatedであり、将来的には廃止される見込みに、C++11ではなっていた。
さて、C++14では、十分に猶予を与えたし、そろそろ廃止してもよかろうという論調になってきた。すなわち、五原則の成立だ。コピーコンストラクター、コピー代入演算子、ムーブコンストラクター、ムーブ代入演算子、デストラクターの5つの特別なメンバー関数のうち、どれかひとつをユーザー定義した場合、残りもユーザー定義しなければならない。なぜならば暗黙にdefault化されないからだ。
// 五原則の例
struct X
{
~X() { }
}
int main()
{
X x1 ;
X x2 = x1 ; // エラー、五原則
}
ただし、五原則を導入すると、既存のコードで、暗黙の宣言に頼っているコードが壊れてしまう。C++規格では、互換性を壊す変更は、相当に慎重になるので、すでにdeprecated扱いになったとはいえ、たやすく消すことはできない。文字列リテラルから暗黙にconst性を失わせる仕様の除去にも、15年かかった。
// 文字列リテラルからconst性をなくす例
int main()
{
// well-formed in C++03
// ill-formed in C++11
char * ptr = "hello" ;
}
この五原則は、ぜひともC++14に入って欲しい。deprecated仕様は、もう何年もの猶予を与えたのだから、十分すぎる。
[PDFは世界一おバカなフォーマット] N3840: A Proposal for the World's Dumbest Smart Pointer, v3
世界一おバカなスマートポインター、observer_ptrの提案。
生のポインターというのは、意図がわかりにくい。たとえ、所有して管理という概念がないとしても、ポインタークラスを標準ライブラリに入れたほうがいいのではないか。そうすれば、そのオブジェクトは管理されていないポインターであるという意図が明確になる。
// observer_ptrの例
#include <memory>
int main()
{
// std::observer_ptr<int> ptr = new int(0) ; と同じ
auto ptr = std::make_observer( new int(0) ) ;
auto p2 = ptr ; // コピーできる
delete ptr ;
}
observer_ptrは、ポインターを所有しないし、管理することもない。そのため、自由にコピーもできる。ポインターが動的に確保されたストレージをさす場合、その解放は利用者の責任である。
生のポインターを直接使うより、意図がわかりやすくなる。ただし、別に生のポインターでもわかりにくくならないのではないかという標準会員の重鎮もいるし、議論は分かれている。
[PDFの使用も非推奨] N3841: Discouraging rand() in C++14
std::random_shuffleとstd::randを非推奨にする文面を追加する提案。
std::randは、とてつもなく使いにくい設計であり、広く誤用されている。例えば、以下のような間違ったコードが使われている。
// 間違ったサイコロの実装
int roll( )
{
return std::rand()%6 + 1 ;
}
このコードは間違っている。なぜならば、剰余は均一分布しないからだ。ある乱数から、特定の範囲の値を数学的に信頼できるほどの精度で均一分布させたい場合は、もっと工夫が必要になる。このような間違ったコードは、世の中のソフトウェアに乱用されていて、世の中のソフトウェアに正しくない乱数をもたらしている。このような誤用をしやすい時代遅れの設計のstd::randは、使用を非推奨にすべきである。
また、イテレーターの組の中の要素を、ランダムにシャフルするアルゴリズム、std::random_shuffleも、std::randの利用を前提にしたオーバーロードがあり、また、独自の乱数を渡す部分も、非常に汚い設計になっているので、これも同じく非推奨にする。
規格では、注記として、「randの使用は、故に、移植性がなく、予期できず疑わしい品質とパフォーマンスである」と書かれるようになった。
読者は、std::randやstd::random_shuffleのような時代遅れの設計のライブラリを使ってはならない。std::randの代わりには、C++11に新しく入った素晴らしい設計の乱数ライブラリを使うべきであるし、std::random_shuffleの代わりには、C++11で新しく追加されたstd::shuffleを使うべきである。
[標本採取されないべきPDF] N3842: A sample Proposal
sample(標本採取)というアルゴリズムの追加。このアルゴリズムは、値の集合の中から、ランダムで一部を取り出すものである。このアルゴリズムは、Knuth先生のThe Art of Computer Programming, Volume 2で、アルゴリズムS(“selection sampling technique”) とアルゴリズムR(“reservoir sampling”)と呼ばれている。
ちなみに、vol. 2でアルゴリズムBと呼ばれているのは、C++11に入った<random>にある、std::knuth_bである。
アルゴリズムS(Selection sampling)は、標本を採取する値の集合は、Forward iteratorを必要とし、採取した標本を書き出すイテレーターには、Output Iteratorを必要とする。
アルゴリズムR(Reservoir sampling)は、標本を採取する値の集合は、Input Iteratorを必要とし、採取した標本を書き出すイテレーターには、Random Access Iteratorを必要とする。
提案では、アルゴリズムS版とR版で名前を分けるのではなく、実引数に渡したイテレーターのカテゴリーにより、タグディスパッチをして自動的に適用可能なアルゴリズムを選ぶ設計になっている。
template< class PopIter, class SampleIter, class Size, class URNG >
SampleIter
sample( PopIter first, PopIter last, SampleIter out, Size n, URNG&& g )
{
using pop_t = typename iterator_traits<PopIter>::iterator_category;
using samp_t = typename iterator_traits<SampleIter>::iterator_category;
return __sample( first, last, pop_t{},
out, samp_t{},
n, forward<URNG>(g) );
}
Knuth本のアルゴリズムRは、安定しているが、追加の処理が必要になる。SGI版のSTLにあるsampleアルゴリズムは、不安定だが追加の処理をしない。論文ではこの点を注記しているが、C++のアルゴリズムR版のsampleが、どのように実装されるかについては言及していない。
[江添フレンドリーではないPDF] N3843: A SFINAE-Friendly std::common_type
[江添フレンドリーではないPDF] N3844: A SFINAE-Friendly std::iterator_traits
std::common_typeと、std::iterator_traitsをSFINAEフレンドリーにする提案。
たとえば、以下のようなコードは、C++11では違法(コンパイルエラー)になる。
// C++11では違法
using type = std::common_type<int, int *>::type ;
なぜならば、int型とint *型には、共通の変換できる型がないからだ。
しかし、このようなコードが違法になると、SFINAEの文脈で、問題になる。
// SFINAEとはならない例
// 共通の型がある場合のみ候補に上がるべき関数テンプレート
template < typename T1, typename T2,
typename COMMON_TYPE = std::common_type<T1, T2>::type >
void f( T1, T2 ) { }
// その他の関数fのオーバーロード
int main()
{
int a1 = 0 ;
int * a2 = nullptr ;
f( a1, a2 ) ; // コンパイルエラー
}
このコードでは、SFINAEを使って、共通の型がある場合のみ、オーバーロード解決の候補に上がるテンプレート関数fを宣言しようとしているが、残念ながら、common_typeは共通の型がない場合、内部でill-formedになってしまうので、このコードは通らない。
N3843は、共通の型がない場合にも、内部で勝手にill-formedにならず、しかもSFINAEの文脈で使えるよう、ネストされた型名を宣言しないように、std::common_typeを変更しようという提案だ。
これと全く同じ理由で、std::result_typeは、以下の提案論文の採択により、すでにSFINAEフレンドリーに変更されている。
N3462: std::result_of and SFINAE
result_typeと同様に、common_typeとiterator_traitsもSFINAEフレンドリーになるべきである。この提案は採択されてほしい。
ちなみに、この論文には、ブインの提督、銀天すばる (SubaruG)さんがこの問題を取り上げたブログ記事にも言及している。
- A Blog Post
- Although written in Japanese, the author shows an
invoke
function defined withresult_of
that fails to compile because it (result_of
) is not SFINAE-friendly. It also calls outstd::common_type
as another trait that would benefit from being SFINAE-friendly.
現状、 std::result_of や std::common_type といった「型を取得するメタ関数」は、与えられた型が前提条件を満たさない場合、クラステンプレートの内部でコンパイルエラーとなるため、 SFINAE に使うことが出来ません
[PDFは割り切れない思い] N3845: Greatest Common Divisor and Least Common Multiple
最大公約数を計算するgcdと、最小公倍数を計算するlcmを<cstdlib>に追加する提案。
最大公約数や最小公倍数のアルゴリズムは、義務教育でも習うほど初歩的なものであるが、たとえ実装が一行にせよ、コードを書いて、正しく動くかどうかテストするのは難しい。しかも、このような基本的な関数の用途はかなり広い。標準ライブラリに存在するべきだ。
提案されているgcdとlcmは、constexpr関数かつ関数テンプレートで、二つの整数型の引数を取る。
また、gcdとlcmを実装するために、constexpr関数でテンプレート関数版のabsも追加される。
その実装は、コード量だけで言えば、とても短い。この記事に引用できるほどだ。
// N3845のリファレンス実装例
// 仮にclib名前空間内に置く
namespace clib {
// 任意の整数型で動くテンプレート版のabs
template< class T >
constexpr auto abs( T i ) -> enable_if_t< is_integral<T>{}(), T >
{
return i < T(0) ? -i : i; }
}
// 二つの実引数が整数型であるかどうかを確認し、また共通の型を返すメタ関数common_int_t
template< class M, class N = M >
using common_int_t = enable_if_t< is_integral<M>{}() and is_integral<N>{}()
, common_type_t<M,N>
> ;
// 最大公約数
template< class M, class N >
constexpr auto gcd( M m, N n ) -> common_int_t<M,N>
{
using CT = common_int_t<M,N>;
return n == 0 ? clib::abs<CT>(m) : gcd<CT,CT>(n, m % n);
}
// 最小公倍数
template< class M, class N >
constexpr auto lcm( M m, N n ) -> common_int_t<M,N>
{ return abs((m / gcd(m,n)) * n); }
いくら短いとはいえ、このようなコードをコピペして使いまわすのは不適切だ。このような基本的でよく使われる関数は、標準ライブラリに存在するべきなのだ。
[PDFは拡張せずに滅びるべき] N3846: Extending static_assert
C++11で追加されたstatic_assertは、必ず文字列リテラルを指定しなければならない。
// C++11のstatic_assertの例
static_assert( true, "This can't be possible!") ;
しかし、なぜ指定しなければならないのだろうか。単にコンパイル時assertを書きたいだけで、特に文字列を指定したくない時でも、たとえ空にせよ、文字列リテラルを指定しなければならないのだ。
さらに、現在のBOOST_STATIC_ASSERTの実装は、コンパイラーにstatic_assertが実装されている場合は、以下のようになる。
// BOOST_STATIC_ASSERTの実装
#define BOOST_STATIC_ASSERT(B) static_assert(B, #B)
これはつまり、以下のように書くと、
BOOST_STATIC_ASSERT(( sizeof(int) == 4 )) ;
以下のように展開される。
static_assert(( sizeof(int) == 4 ), "( sizeof(int) == 4 )") ;
多くのプログラマーにとって、static_assertに指定した式を文字列として読め、しかもそれがデフォルトとなっているのは、便利だ。
このような文字列リテラルを指定しないstatic_assert、デフォルトで式を文字列化するstatic_assertは、何年もの間、多数の人から、要望されてきた。
実際、BOOST_STATIC_ASSERTと同等のマクロは、実に大勢のものによって、再発明されてきた。これは、明らかに利用者がこのような機能を必要としている証拠である。
とはいえ、式を文字列化するというのは、ただでさえコンパイラーの実装が難しいC++では、とても難しいのだ。Phase of Translationをかける前の文字列でなければならない。あるいは、トークン列に分割したものを再び文字列化するか。
それ故、多くのコンパイラー屋は、式を文字列化するという機能をコア言語でサポートすることに難色を示している。「文字列リテラルを取らない文法を追加して、出力される文字列は実装依存とするならば反対はしないが、式を文字列化するというのは、C++コンパイラーの実装の都合上、とても難しい」という意見だ。これはプリプロセッサーの仕事だと主張するコンパイラー屋が多い。
一方、Cプリプロセッサーは醜悪であり、将来的には廃止されるべきであり、そのような機能に依存してはならないと江添は考える。
ところで、本当に式の文字列化は有益だろうか。実際にstatic_assertに引っかかったならば、ソースコードから問題のstatic_assertとその前後を確認するはずで、static_assertの式が文字列化されて、コンパイルエラーメッセージに含まれたところで、それほど意味はないのではないか。つまり、
#define STATIC_ASSERT(B) static_assert(B, #B)
このような実装と、
#define STATIC_ASSERT(B) static_assert(B, "ouch")
この実装で、現実的になにか違いはあるだろうか?
いやまて、そもそも、文字列以外の任意個の値を表示したい需要だってあるだろう。独自拡張を含めれば、様々な値がコンパイル時に取得できるのに、コンパイル時に値を表示できないのは残念すぎる。
static_assertがprintf風のフォーマットをサポートしてたら嬉しいって誰かが言ってた。
と、論文では収集がつかない意見の噴出を紹介している。
その上で、論文は、いくつかの文面案を提案している。
- 案1 (Daniel Krüglerの提案)
-
もし、文字列リテラルが与えられない場合は、結果のdiagnosticメッセージは、定数式のテキストである。
BOOST_STATIC_ASSERT相当の機能をコア言語で直接サポートする提案
- 案2 (Mike Millerの提案)
-
もし、文字列リテラルが与えられない場合は、結果のdiagnosticメッセージは、実装依存である。
すでにトークン列化されてるものを、再び文字列に戻したくねーよというコンパイラー屋の提案。
- 案4
-
コンマで区切って任意個の値を与え、それを人間が読める形で表示する提案
文面案の全訳は面倒なので、コードで示す。
// 案4の利用例 static_assert( sizeof(int) == 4, "Oops, the size of int is not 4! Use this:", sizeof(int), " Also check the size of long, just in case :D :", sizeof(long) ) ;
これは・・・はて、どうだろう。
何にせよ、文字列を指定しないstatic_assertは欲しいところだ。
[PDFは簡単ではない!] N3847: Random Number Generation is Not Simple!
すでに提案中の、初心者にも使いやすい乱数ライブラリ、std::pick_a_numberに対する提案。
まず、pick_a_numberを関数テンプレートにし、浮動小数点数型も扱えるようにする。
// N3847提案のpick_a_number
#include <random>
int main()
{
int a = std::pick_a_number( 1, 6 ) ;
double b = std::pick_a_nubmer( 1.0, 6.0 ) ;
}
また、関数オブジェクトとして呼ぶと乱数を返すクラスも提供する。これにより、<algorithm>に渡しやすくなる。
さらに、乱数を返すイテレーターも提供する。これにより、copy_nなどのアルゴリズムに渡せる。
クラスがテンプレートでないのは気になるところだ。
N3848: Working Draft, Technical Specification on C++ Extensions for Library Fundamentals
std::optionalのドラフト文面。
N3849: string_view: a non-owning reference to a string, revision 6
文字列をラップするクラス、string_viewの提案。
多くの場合、文字列を扱うとき、その文字列がどのように実装されているかは、どうでもいいことだ。std::stringかもしれないし、null終端されたcharの配列かもしれない。重要なのは、それが文字列として振る舞うことであって、実装方法の詳細ではない。そこで、様々な実装方法の文字列をラップして、共通の操作を提供するクラスが欲しい。
このような需要は極めて一般的なもので、GoogleとかBloombergとか、ChromiumとかLLVMなどといった企業やプロジェクトで、ほぼ同等機能のライブラリが、独立して再発明され続けている。それならば、標準ライブラリとして存在すべきだ。
string_viewは、具体的な文字列の実装を参照して、std::basic_string風のインターフェースで見るためのクラスであり、変更することはできない。変更できるようにすると、文字列リテラルでstring_viewを初期化することができなくなる。それに、文字列を変更するということは、往々にして、文字列の長さも変えなければならない。それには具体的な実装による処理が絡んでくる。そのため、文字列を変更するという需要はない。事実、LLVMでも、文字列を変更する版のライブラリを追加する需要は発生していない。
[PDFはやめてくれ頼む] N3850: Working Draft, Technical Specification for C++ Extensions for Parallelism
<algorithm>に並列実行版を付け加える提案のドラフト文面
この提案では、従来のアルゴリズムに、実行ポリシー(execution policy)を取るオーバーロードが追加される。
標準規格では、三つの実行ポリシーを定義している。すなわち、std::seq, std::par, std::vecであり、それぞれ、シーケンシャル実行ポリシー(sequential execution policy)、並列実行ポリシー(parallel execution policy)、ベクトル実行ポリシー(vector execution policy)である。既存のアルゴリズムは、すべてシーケンシャル実行ポリシーである。
並列実行ポリシーは、処理の実行単位が順序を問わず、未規定のスレッドで非シーケンシャルに実行される。そのため、スレッドローカルストレージなどは、実行の単位ごとに異なる可能性があるし、ロックなども慎重に使わなければ、実行単位の間でデッドロックを引き起こす。
ベクトル実行ポリシーは、処理が未規定のスレッドで実行されるし、またひとつのスレッドで実行された場合、非シーケンシャルである。ベクトル実行ポリシーはさらに制約が強く、実行単位の中では、一切のロックを行ってはならない。
// N3850の例
#include
int main()
{
std::vector v ;
// vに値を入れる
// 従来のsort、暗黙にシーケンシャル実行ポリシー
std::sort( v.begin(), v.end() ) ;
// 明示的にシーケンシャル実行ポリシー
std::sort( std::seq, v.begin(), v.end() ) ;
// 並列実行ポリシー
std::sort( std::par, v.begin(), v.end() ) ;
// ベクトル実行ポリシー
std::sort( std::vec, v.begin(), v.end() ) ;
}
並列実行ポリシーや、ベクトル実行ポリシーが、どのように実装されるかは規定されていないし、それは規格が定めるところではない。
[多次元的に複雑なPDF] N3851: Multidimensional bounds, index and array_view
連続したメモリ配列に対して、多次元配列操作を提供する、array_viewライブラリの提案。Microsoftのライブラリ、C++ AMPを土台としている。
array_viewは、連続したメモリを参照して、あたかも多次元配列を操作しているかのようなインターフェースを提供する。
// N3851時点での提案の例
std::size_t M = 32 ;
std::size_t N = 64 ;
std::vector<float> v( M * N ) ;
std::array_view<float, 2> view{ { M, N }, v } ;
view[{ 3, 3 }] = 0 ;
また、array_viewは、bounds_iteratorにより、各要素に対して、一次元的なアクセスを提供している。
// N3851時点でのイテレーター
std::bounds_iterator<2> first = std::begin( view.bounds() ) ;
std::bounds_iterator<2> last = std::end( view.bounds() ) ;
float sum = 0.0 ;
std::for_each( first, last,
[]( std::index<2> index )
{
sum += view[index] ;
}
string_viewとは違い、array_viewはmutableである。つまり、参照先を変更することができる。これは、文字列と多次元配列という違う概念による違いだ。LLVMでも、2011年2月に追加したimmutableなArrayRefに対して、mutable版のMutableArrayRefを追加したのが2013年1月。StringRefに対しては、いまだにMutableStringRefを追加する需要がない。
前回、各国から送られたNational Body CommentへのC++標準化委員会への返答。日本は一件も送っていない。
特に興味深いNBコメントの結果だけ紹介する。
CH2: スイスは、C++14はマイナーアップデートであり、ドラフトの質に影響するような大規模な変更をやめろと意見した。これは受け入れられた。その結果、optionalとdynarrayは、標準規格ではなく、TS(Technical Specification)という形で制定することになった。いずれは、標準規格にも取り入れられるだろう。
US15: アメリカ合衆国は、N3655により、すべてのtypetrait<...>::typeは、エイリアステンプレートを使って、typetrait_t<...>と書けるようになった。さっそく、規格の文面でも置換を行おうと意見した。これは採択された。
数値区切りは、一旦却下されたものの、各国から、入れろ、考えなおせというNBコメントが相次いだために、採択された。
// 数値区切りの例
int x = 1000'000'000 ;
ES8: グローバルなoperator delete[]に、第二引数にstd::size_t型で、ストレージのサイズを取るオーバーロードが追加された。これは、従来メンバー関数にはあったが、なぜかグローバルな解放関数にはなかったものだ。ストレージのサイズが渡されることで、一部のメモリアロケーターの実装の解放時の処理が、とても効率的になる。
GB4: 文面の変更により、ストレージ確保と例外がからむと、従来は余計にストレージが確保されて、もともとメモリーリークしていたコードが、さらにリークする可能性があるというイギリスの意見に対し、core issue 1786が作られた。
ES10, US8: [[deprecated]]は採択されたが、CDに入れ忘れているぞというスペインの意見に対し、N3394をCDに適用する対応がなされた。
US9: 現行ドラフトでは、実行時サイズ配列の添字が0と評価された場合、例外を投げるとしている。これは合法なC99のコードをC++14では実行時に失敗させてしまう。既存のC99コードには、添字0のコードが山ほどある。実際に、G++で実行時サイズ配列を実装して、既存のコードに試してみると、多くが壊れてしまった。添字が負数である場合はいいが、0の場合は認めるべきであるというアメリカ合衆国が意見した。これは採択された。core issue 1768
CH5: プリプロセッサーマクロ、__func__が関数の中ではないlambda式(名前空間スコープの中の初期化子の中のlambda式)の中で使えるかどうか、文面上疑問であるというスイスの意見。これは文面を修正することに決まった。
US13: 非volatileローカル変数をreturnするときは、常にムーブでいいだろうという米国の意見。これは採用された。
ES11: forward_as_tupleがconstexprではないというスペインの意見。constexprとなることに決まった。
GB9: C11では、忌まわしき危険なgetsが取り除かれた。まだC++はC99規格を参照しているとはいえ、getsに関しては、C++でも廃止すべきであるというイギリスの意見が採用された。
FI4: これは却下されたが面白かったので紹介。戻り値の型推定は、変換関数にも適用できる。
// 変換関数に関数の戻り値の型推定を適用する例
struct Zero
{
operator auto() { return 0 ; }
} ;
int main()
{
Zero zero ;
int x = zero ;
}
N3853: Range-Based For-Loops: The Next Generation
range-based for loopは、従来のforに比べて、格段に使いやすくなった。
// 昔のforループ
void once_upon_a_time( std::vector<int> & range )
{
for ( std::vector<int>::iterator first = range.begin(),
std::vector<int>::iterator last = range.end() ;
first != last ; ++first )
{
std::cout << *first << '\n' ;
}
}
// 今のforループ
void and_now( std::vector<int> & range )
{
for ( int elem : range )
{
std::cout << elem << '\n' ;
}
}
なるほど、これはわかりやすくなった。しかし、要素の型を指定するのが面倒だ。そもそも、要素の型はコンパイル時に決定できるわけだ。重要なのは型ではない。すると、以下のように書ける。
// auto specifierの利用
for ( auto elem : range ) ;
なるほど、これは実にわかりやすい。ただし、これには問題がある。
// Range-based for loopとauto specifierの問題点
void f( std::vector< std::string > & range )
{
for ( auto elem : range )
{
std::cout << elem << '\n' ;
}
}
これは動くが、いちいちにstd::stringオブジェクトのコピーが発生してしまう。そして、従来のC++プログラマーは、このコードによってコピーが発生しているということを、見逃しやすい。これは問題だ。
では型名を書くかというと、それも問題なのだ。例えば以下のようなコードが問題になる。
// コピーが発生してしまうコード
void f( std::map< const int, int > & range )
{
for ( std::pair< int, int > const & elem : range )
{
// ...
}
}
これにはコピーが発生してしまう。なぜならば、rangeの要素は、std::pair< const int, int >だからだ。型の変換のためにコピーが発生してしまうのだ。
型名を明示的に指定するのは問題が多い。しかし、autoでもコピーが発生してしまう。ではどうすればいいのか。
"for ( auto & elem : range )"は、まだいくらかマシだ。しかし、これもvector<bool>のようなプロクシーイテレーターに対応できない問題がある。
// プロクシーに対応できない例
void f( std::vector<int> & v )
{
for ( auto & elem : v )
{
std::cout << elem << '\n' ;
}
}
int main()
{
std::vector<int> non_proxy{ 1, 2, 3 } ;
f( non_proxy ) ; // well-formed
std::vector<bool> proxy{ true, true, true } ;
f( proxy ) ; // ill-formed!
}
vector<bool>は、規格上、だいぶ変わった実装をしなければならない。この実装はプロクシーと呼ばれてる技法が使われている。イテレーター一つ一つに対応する要素へのオブジェクトはない。そもそも、boolはたったの1ビットの情報で表現できるのだ。であれば、charの配列のようなストレージを用意しておき、その1bitづつに、要素一つを対応させればいいのだ。そして、イテレーターで変換を行う。
問題は、そういう場合に、bool一つ一つに対応するオブジェクトがないので、イテレーターはかなり不自然な挙動をする。もちろん、規格の要件通りなのだが、やはり不自然なことは不自然だ。
"for( auto const & elem : range )"は、ほとんどのプロクシー実装に対応できるが、これでは書き換えることができない。
実は、この問題を解決できる最高の方法があるのだ。"for ( auto && elem : range )"である。
auto &&は、どんなものにでも対応できる、万能の指定子なのだ。
しかし問題は、auto &&を使うには、プログラマーはrvalue referenceやauto specifierやTemplate Argument Deduction(とくにテンプレート名にrvalue referenceが用いられた場合の不思議な挙動)について精通していなければならない。それは初心者にはいかにも無理だ。
簡単に使えるように作られたはずの機能が、実際には簡単に使えないときている。これは簡単に使えるように、言語を拡張すべきだ。すなわち、新しい文法、"for ( elem : range )"の提案だ。
"for ( elem : range )"は、自動的に、"for ( auto && elem : range )"と書いたものと同様にみなされる。これにより、初心者も詳細を理解せずに使うことができる。文法上必要なゴミもなくなり、とてもわかりやすい。
N3854: Variable Templates For Type Traits
C++14では、エイリアステンプレートを用いて、従来のtype traitsの使いやすいラッパーを追加している。
// 新旧比較
std::add_pointer<int>::type old = nullptr ;
std::add_pointer_t<int> latest = nullptr ;
余計な、ネストされた型名、::typeがいらなくなるので、とても書きやすくなる。
type_traits_tは、エイリアステンプレートを用いたラッパーであり、以下のように書くことができる。
template < typename T > add_pointer_t = std::add_pointer<T>::type ;
C++14には、変数テンプレートも追加されたので、これの値版を追加する提案。すなわち、
// 新旧比較
bool old = std::is_integer<int>::value ;
bool latest = std::is_integer_v<int> ;
::valueが必要なくなる。
この実装は、例えば以下のようになる。
template < typename T >
constexpr bool is_integer_v = is_integer<T>::value ;
これは入って当然の提案だ。
[PDFは忘却されるべき] N3856: Unforgetting standard functions min/max as constexpr
これは異例に短い論文。PDFである理由が全くわからない。
中身は、min/maxをconstexprにする提案だ。ついうっかり入れ忘れていたらしい。
岡山の某陶芸家がおお喜びする様が目に見えるようだ。
[PDFは改良の余地なし] N3857: Improvements to std::future<T> and Related APIs
非常に基礎的な機能しかなかった貧弱なfutureを大幅に改良する提案。便利な機能が追加される。この提案は、次にN3858と対になっている。
まず、futureの基本的な使い方をみてみよう。
// C++11のfuture
#include <future>
int main()
{
auto f = std::async( [](){ return 123 ; } ) ;
auto result = f.get() ;
}
futureは、基本的にはこれだけしかできない。futureは、値をgetで取得する。そして、getは、値がfutureに対応するpromiseに設定されるまでブロックする。それだけだ。
then
futureに値が決定された後に、処理をしたいことはほとんどだろう。そもそも、値が決定されたら、自動的に処理をして欲しいはずだ。また、その処理は、再び非同期で実行されて欲しいかも知れない。すなわちfutureを返したい。C++11では、以下のように書かなければならない。
// C++11の例
#include <future>
int main()
{
// まあ、これはいいか
auto f1 = std::async( [](){ return 123 ; } ) ;
auto r1 = f.get() ;// え、俺がやるの?
// またかよ・・・
auto f2 = std::async( [&](){ return r1 + 456 ; } ) ;
auto r2 = f2.get() ; // で、また俺がやるの?
}
たったの一回の後処理だけでこれなのだ。続けて何度も非同期な後処理をしたい場合、記述が煩雑になり面倒だ。
このような煩雑なコードは、thenにより簡略化できる。
// then
#include <future>
int main()
{
auto f2 = std::async( []() { return 123 ; } )
.then( []( auto f ){ return f.get() + 456 ; }
)
auto r2 = f2.get() ;
}
thenならば、futureの結果非同期に実行される後処理を、future.then(..).then(...).then(...)と続けて、簡潔に書ける。
unwrap
futureのネスト、すなわち、future<future<int>>>のようなことは、起こりうることである。では、ネストしたfutureを取り出すにはどうすればいいのか。getを使うとブロックしてしまうおそれがある。非同期に中身を取り出したい。しかし、futureから中身を取り出すために非同期処理を書くのは面倒だし、例外も面倒を見なければならない。非同期処理をした結果を得るのに非同期処理を自前で書くという、わけのわからないことをしなければならない。
unwrapは、そのような非同期のfutureの展開をしてくれるものだ。
// unwrapの例
#include <future>
int main()
{
// outer_futureはstd::future< std::future< int > >
auto outer_future = std::async( []{
return std::async( [] {
return 123 ;
} ) ; // inner
} ) ; // outer
auto inner_future_proxy = outer_future.unwrap() ;
inner_future_proxy.then([]( auto f ) {
auto result = f.get() ;
} ) ;
}
is_ready
C++11のfutureには、getしかない。getは、まだ値が決定されていない場合、問答無用で待ちに入る。しかし、多くの場合、値が決定されているかどうかをブロックせずに確認だけしたいはずだ。こんな初歩的な機能が、C++11になかったのは、どうしようもないことなのだが、その機能が入る。is_readyだ。
// is_readyの例
#include <future>
void do_something_or_other( std::future<int> f )
{
if ( f.is_ready() )
{
int value = f.get() ; // ブロックしない
}
else
{
// getはブロックするので、なにか別のことをする
}
}
時間を指定する、wait_forやwait_untilはあったが、なぜこれはなかったのか。
when_any
複数のfutureのどれか一つでも完了した場合に値が決定するfutureを返す。
これには2バージョンあり、future<vector<future<T>>>を返すタイプと、future<tuple<future<T>>>を返すタイプがある、複数のfutureをイテレーターで渡すとvector版が、実引数で渡すとtuple版が返される
// when_anyの例
#include <future>
int main()
{
std::future<int> futures[] = {
std::async( []{ return 1 ; },
std::async( []{ return 2 ; },
std::async( []{ return 3 ; }
} ;
// std::future< std::vector< std::future<int> > >
auto vec_future = std::when_any( std::begin(futures), std::end(futures) ) ;
// std::future< std::tuple< std::future<int>, std::future<int>, std::future<int> > >
auto tuple_future = std::when_any( futures[0], futures[1], futures[2] ) ;
vec_future.then([]{
// どれかが完了している
} ) ;
}
when_all
複数のfutureのすべてが完了した時に完了するfutureを返す。
make_ready_future
futureを作るとき、すでに値が決定されていることが、しばしばある。しかし、値を設定済みのfutureを作るのは、C++11では意外と面倒だ。まず、promiseを作り、そのpromiseに値をセットして、そのpromiseからfutureを得なければならない。
// 値をセット済みのfutureを作る
template <typename T >
std::future< typename std::decay_t<T> > make_ready_future( T && value )
{
std::promise< typename std::decay<T>::type > p ;
p.set_value( std::forward<T>( value ) ) ;
return p.get_future() ;
}
こんな基礎的なことを、わざわざ自前で書きたくない。間違いの元だ。最初からこれが標準ライブラリにあればよい。
[PDFを継続する必要はない] N3858: Resumable Functions
Resumable Functionの提案。
Resumable Functionとは、ブロックする実行を途中で中断して、関数の呼び出し元に、制御を返し、後で自動的に実行が再開されて、結果を得ることができる関数だ。futureにメンバー関数thenを付け加えるのもいいのだが、resumable functionがコア言語でサポートされていると、非同期コードが、とても簡潔に書けるようになる。
future.thenを使うと以下のように書かなければならないコードが、
// future.thenの例
future<int> f(shared_ptr<stream> str)
{
shared_ptr<vector<char>> buf = ...;
return str->read(512, buf)
.then([](future<int> op)
// lambda 1
{
return op.get() + 11;
});
}
future<void> g()
{
shared_ptr<stream> s = ...;
return f(s).then([s](future<int> op)
{
s->close();
});
}
以下のように簡潔に書けるようになる。
// resumable functionの例
future<int> f(stream str) resumable
{
shared_ptr<vector<char>> buf = ...;
int count = await str.read(512, buf);
return count + 11;
}
future<void> g() resumable
{
stream s = ...;
int pls11 = await f(s);
s.close();
}
非同期コードを書くときは、同期コードをまず書いてから、それを非同期コードに直すことも多いので、ほぼ同期コードと同じように書けるコア言語によるresumable functionのサポートは、コードの記述をとても簡単にする。
resumable functionの文法と機能について簡単に解説すると、以下のようになる。
resumable functionの宣言は、かならずresumable指定子を使わなければならない。resumable指定子とは、resumableというキーワードだ。
// resumableキーワード
std::future<int> f() resumable ;
[]() resumable {} ;
resumable指定子の位置は、関数とメンバー関数の場合、リファレンス修飾子の後、例外指定の前だ。
// 関数のresumable指定子の位置
struct S
{
std::future<int> f() & resumable noexcept ;
} ;
lambda式の場合、mutableの後、例外指定の前になる
// lambda式のresumable指定子の位置
[]() mutable resumable noexcept -> std::future<int>{ return 0 ; }
もし、resumable指定子のあるlambda式で、戻り値の型が省略された場合、戻り値の型は、Tをreturn文から型推定された型とすると、std::future<T>になる。
// lambda式かつresumable functionの戻り値の型推定
// 戻り値の型はstd::future<int>
[]() resumable { return 0 ; }
resumable functionには、いくつかの制約がある。大きな制約を抜き出すと、
- resumable functionの戻り値の型は、std::future<T>か、std::shared_future<T>でなければならない。Tがvoid型のときは、値を返さないresumable functionとして認識される。
- C言語からある可変引数(...)は使えない。Variadic Templatesの関数パラメーターパックは使える。
resumable functionの関数の本体では、await演算子が使える。これも新しいキーワードで、文法上、式になる。
await expr
したがって、条件を満たす式ならば、どこにでも使える。awaitを使った時点で、関数の実行は中断され、その時点で呼び出し元に処理が戻る。そして、await演算子のオペランドが終了した時点で、resumable関数の実行が再開される。
//awaitの例
std::future<void> f() resumable
{
// 事前の処理
int r1 = await std::async( []{ return 0 ; } ) ;
// 事後の処理
int r2 = await std::async) [] { return 0 ; } ) ;
// さらに処理
return ;
}
await演算子の特徴で、特に興味深いものを抜き出すと、
- await演算子は、resumable functionの中か、decltype式の中でしか使えない。
- await演算子のオペランドの式を評価した型は、future<T>かshared_future<T>か、あるいは、このどちらかの型に暗黙に変換可能でなければならない
- await演算子は、例外ハンドラーの中では使えない。
- mutexのロックを取得している状態でawait演算子を実行してはならない。
- await演算子を評価した結果の型は、futureかshared_futureをgetした結果の型になる。もし、型がvoid型の場合、await演算子は他の式の中では使えない。
論文では、resumable functionの実装方法の例についても言及している。
とても簡単な実装は、サイドスタックと呼ばれる方法だ。これは、resumable functionが発動する際に、専用のスタック用のメモリーを確保し、resumable functionに入る前と後で、スタックポインターのすげかえを行う。これにより、ローカル関数を始めとしたスタックに依存するものが、問題なく動くようになる。ただし、スタック用のメモリは、ある程度の大きさの連続したメモリ空間を必要とするため、効率が悪い。
より効率的な実装としては、メモリをヒープ上に動的に確保して、参照カウンターで管理する方法がある。しかし、この方法は、実装が難しい。
論文では、将来の拡張の可能性についても言及している。
まず、汎用化だ。futureやshared_future以外の型も、メンバー関数getを持つとか、thenを持つとか、is_readyを持つなどすれば、認めるという案だ。これにより、汎用的に書ける。
そして、ジェネレーターだ。C#やPythonではすでに実用化されているパラダイムだが、値の集合を一つ一つ遅延して計算したいときなどに、ジェネレーターを使うと、とても自然に書ける。このために、yieldというキーワードを導入し、yieldが実行されるたびに関数から処理を戻すような機能を提供する。
ジェネレーターは、非同期やスレッドとは関係がないが、とてもおもしろくて便利なパラダイムだ。ぜひともジェネレーターは、将来、議論されて欲しい。
[空間の間に消えてほしいPDF] N3859: Transactional Memory Support for C++
トランザクショナルメモリー(Transactional Memory)の提案。
トランザクショナルメモリーとは、並列実行における複数のストレージへの排他的なアクセスを、より簡単に行うための機能だ。
複数のスレッドから、複数のストレージに書き込む場合は、mutexなどの方法で、排他的なロックをかけなければならない。
// mutexの例
int x = 0 ;
int y = 0 ;
// x, yにアクセスする際は、かならずmをロックすること
std::mutex m ;
void f()
{
{
std::lock_guard< std::mutex > lock(m) ;
++x ;
++y ;
}
}
しかし、このような明示的にロックするコードは、書きにくい。わざわざロック、アンロックしなければならないし、mutexオブジェクトの管理も面倒だ。もっと簡単に書けないものか。
そこで、Transactional Memoryの出番だ。これはコア言語の文法として提供される機能なので、とても簡単に使えるようになっている。
// N3859提案のTransactional Memoryの例
int x = 0 ;
int y = 0 ;
void f()
{
synchronized
{
++x ;
++y ;
}
}
Transactional Memoryを使えば、自前でmutexオブジェクトを管理せずに、とても簡単に複数のオブジェクトへの排他的なアクセスを書けるようになる。
これは同期ブロック(synchronized block)と呼ばれている。その文法は、キーワードsynchronizedに続けてブロック文を書く。すべてのスレッドのすべての同期ブロックは同期する。つまり、同期ブロックの評価は、あたかもひとつのスレッドの中で逐次実行したかのように振る舞う。つまり、正しく同期ブロックの中から読み書きすれば、複数のスレッドからであっても、競合は一切起こらない。
Transactional Memoryには、もうひとつ。アトミックブロック(Atomic Block)というものがある。これには、三種類ある。
// N3859提案のAtomic Block三種類
atomic noexcept { /* ... */ }
atomic commit_except { /* ... */ }
atomic cancel_except { /* ... */ }
アトミックブロックの文法は、キーワードatomicに続けて、noexcept/commit_except/cancel_exceptのいずれかのキーワードを書き、その後にブロック文を書く。
atomic noexceptは、アトミックブロックの中から外に例外を投げないことをユーザーが保証する。
// atomic noexcept
atomic noexcept
{
try
{
// ...
}
catch( ... )
{
// 絶対に外に例外を投げないように握りつぶす
}
}
atomic commit_exceptは、アトミックブロックから例外によって抜けだした際に、トランザクションをコミットして、例外を投げる。つまり、それまでの副作用をブロックの外から観測できるようにする。
// atomic commit_except
int x = 0 ;
int y = 0 ;
void f()
{
try
{
atomic commit_except
{
++x ;
throw 0 ;
++y ;
}
} catch( ... ) { }
// この時点で、他のスレッドを考慮に入れなければ
// xはインクリメントされている
// yは初期値のまま
}
atomic cancel_exceptは、アトミックブロックから例外によって抜けだした際に、それがTransaction-safeな例外であれば、トランザクションをキャンセルして、例外を投げる。つまり、それまでの副作用をなかったことにする。
// atomic cancel_except
int x = 0 ;
int y = 0 ;
void f()
{
try
{
atomic cancel_except
{
++x ;
throw 0 ;
++y ;
}
} catch( ... ) { }
// この時点で、他のスレッドを考慮に入れなければ
// xは初期値のまま
// yは初期値のまま
}
transaction-safeな例外というのは、N3859提案の段階では、bad_alloc, bad_array_length, bad_array_new)length, bad_cast , bad_typeid , スカラー型となっている。議論の上で、この制限を緩和することも考えているそうだ。
アトミックブロックの中の副作用は、アトミックブロックを抜けた際に、一斉に観測できるようになる。つまり、
// 副作用は一斉に見えるか見えないか
int x = 0 ;
int y = 0 ;
void f()
{
atomic noexcept
{
++x ;
// #1
++y ;
}
// #2
}
#1の時点では、アトミックブロックの中では、xはインクリメントされているようにみえるが、他のスレッドからは、xは初期値のままにみえる。
#2で、すべてのスレッドから、xとyが一斉にインクリメントされたように見える。この点では、同期ブロックもアトミックブロックも変わらない。また、アトミックブロックも、観測できる範囲では、同期ブロックと同じく、順序だって評価されているように見える。
では、アトミックブロックは、同期ブロックとどう違うのか。同期ブロックというのは、単なるプログラム全体で単一のmutexを共有するような想定だが、アトミックブロックというのは、何らかのハードウェア、ソフトウェアによる効率的なトランザクショナルメモリーの実装が行われることが期待されている。もちろん、規格上、どのように実装されるかという詳細は規定していないのだが。
また、規格ではTransaction-safeという概念を提唱し、Transactional Memoryのブロック内で行える処理に制限を設けている。また、関数をtransaction-safeとtransaction-unsafeに分けている。これらはキーワードで明示的に指定することもできる。既存のSTLコンテナーなどの標準ライブラリはtransaction-safeで使えるべきで、対応すべきだという意見も提示している。
[読みづらいPDF] N3861: Transactional Memory (TM) Meeting Minutes 2013/09/09-2014/01/20
トランザクショナルメモリーに関する会議の議事録
[PDF廃止に向けて] N3862: Towards a Transaction-safe C++ Standard Library: std::list
GCC 4.9に実装されたTransactional Memoryの実験的実装を使い、libstdc++のstd::listを、実際にtransaction-safeにしてみる実験。変更は最小限ですんだそうだ。
そのコードはGitHubに上がっている。
残りの論文も、追って解説する。
ドワンゴ広告
この記事はドワンゴの勤務時間中に書かれた。
ドワンゴは本物のC++プログラマーを募集しています。
CC BY-ND 4.0: Creative Commons — Attribution-NoDerivatives 4.0 International — CC BY-ND 4.0