2015-12-11

C++標準化委員会の文書のレビュー: P0110R0-P0119R0

P0110: Implementing the strong guarantee for variant<> assignment

強いvariantの実装の考察。

型Aのオブジェクトを保持するvariantに型Bのオブジェクトを代入しようとした時、型Aのオブジェクトを破棄してから型Bを構築するわけだが、もし、型Bのオブジェクトの構築時に例外が投げられた場合、型Bは構築できていないし、型Aは破棄したあとで、variantはどの型も保持していない無効な状態、あるいは空の状態になる。

variantはこのような無効な状態を作り出さない強い保証を持つべきだとの意見が多い。では、そのような強いvariantの実装はどのようになるのか。

型Bが無例外保証のあるコンストラクターを持つ場合、型Aのオブジェクトを破棄して、型Bのオブジェクトを構築すればよい。構築は絶対に失敗しない。

型Bが無例外保証のあるムーブ構築可能な型であるばあい、つまりstd::nothrow_move_constructible<B>::valueがtrueである場合、型Bのオブジェクトをスタックに構築して、型Aのオブジェクトを破棄して、型Bのオブジェクトをvariantのデータメンバーにムーブ構築して、スタック上の型Bのオブジェクトを破棄する。

型Bに無例外保証がないが、型Aにはある場合、型Aのオブジェクトをスタック上にムーブ構築して退避させ、型Bのオブジェクトを構築すればよい。

型AにもBにも無例外保証がない場合、バッファー上に型Bのオブジェクトを構築して、型Aのオブジェクトを破棄して、バッファー上の型Bをvariantの新しいオブジェクトとする。一般に、variantに2つ以上の無例外保証のない型がある場合、この実装になる。

問題は、そのバッファーはどこに取るべきかということだ。variantのデータメンバーに取ると、variantのサイズが2倍になってしまう。動的確保は好ましくない。すると、一般的にダブルバッファリングと呼ばれている、variantのサイズを2倍にする方法しかない。

emplaceについては、その存在意義から、ダブルバッファリングはせずに、基本的な例外保証のみに止めたほうがよいとしている。

論文著者による強いvariantの実装例が公開されている。

https://bitbucket.org/anthonyw/variant

P0112R0: Networking Library (Revision 6)

Boost.Asioベースのネットワークライブラリの提案。

P0113R0: Executors and Asynchronous Operations, Revision 2

実行媒体をポリシーベースで選択できるexecutorと非同期実行ライブラリの提案

以下のように使う。

// 非同期実行
post( p[] {
    // 処理
} ) ;

// スレッドプール内で非同期実行

thread_pool pool ;

post( pool, []{
    // 処理
} ) ;

asyncのようにfutureを受け取って結果を待つこともできる。

std::future<int> f =
    post( use_future( []{
        return 32 ;
        } ) ;

int result = f.get() ;

似たような提案が乱立していて、executorという用語も別々の意味で使っているので、混同しやすい。

P0114R0: Resumable Expressions (revision 1)

resumable式の提案。

resumable void f( int n )
{
    int i = 1 ;
    while( true )
    {
        std::cout << i << std::endl ;
        ++i ;
        if ( i > n )
            break ;

        break resumeale ;
    }
}

int main()
{
    resumable auto r = f( 5 ) ;

    while ( !r.ready() )
    {
        r.resume() ;
    }
}

コルーチンとresumable関数が分かれていた時の文章も混じっていてわかりにくい。

P0116R0: Boolean conversion for Standard Library types

std::bitset, std::chrono::duration, std::complexにexplicit operator boolを追加する提案。

bitsetは(count != 0)を返す、つまりすべてのビットが立っていない時にfalseを返す。

durationは( *this == zero() )を返す。つまり、ゼロのときにfalseを返す(durationのゼロ表現は結構面倒)

complexは、(real() != T() || imag() != T())を返す。つまり、real()とimag()がともにfalseの場合にfalseを返す(ド・モルガンの法則)

この文書で提案しているのはこの3つだけだが、他のライブラリにもexplicit operator boolを提供する考察もしている。例えば、weak_ptrは妥当なポインターを参照しているかどうか、threadは呼び出し元と同じidかどうか。futureはvalid()など。

weak_ptrは保持しているポインターの値と混同しそうだし、threadの挙動はちょっとわかりにくい気がする。

P0117R0: Generic to_string/to_wstring functions

以下のようなto_string/to_wstringを追加する提案。

template<class Ch, class Tr = char_traits<Ch>, class Alloc = allocator<Ch>, class... Args>
basic_string<Ch,Tr,Alloc> to_basic_string(Args&&... args)
{
    basic_ostringstream<Ch,Tr,Alloc> stream;
    stream.exceptions(ios_base::failbit | ios_base::badbit);
    // 文書はbinary foldの文法を間違えているので修正
    ( stream << ... << forward<Args>(args) );
    return stream.str();
}

template<class... Args>
string to_string(Args&&... args)
{
    return to_basic_string<char>(forward<Args>(args)...);
}

template<class... Args>
wstring to_wstring(Args&&... args)
{
    return to_basic_string<wchar_t>(forward<Args>(args)...);
}

リファレンス実装が文書に乗る程度の大きさ。C++17で入る予定のfold式を使っている。しかも文法を間違えている。

使い方


auto s123 = to_string( 123 ) ;
auto s123456 = to+string( 123, 456 ) ; 

マニピュレーターも使える。

[PDF] P0118R0: Concepts-TS editors report

Concept TSの編集者の報告書。変更点が記されている。

[最高に余白を無駄遣いしているPDF] P0119R0: Overload sets as function arguments

極めて読みづらい冒涜的に失礼なレイアウトだが、内容は面白い。

値を引数にとって処理をして返す関数fのオーバーロード群があるとする。

int f( int ) ;
double f( double ) ;
// ...その他多数

さて、std::transformで、値に関数fを適用したいとする。以下のように書くと動かない。


template < typename Iter >
void g( Iter first, Iter last )
{
    // エラー
    // fはオーバーロード関数群
    std::transform( first, last, f ) ;
}

fというのは単一の関数名ではなく、オーバーロードされた関数群だからだ。

これを解決するには、オーバーロード解決後の関数ポインターを得るように、明示的にキャストしてやる必要がある。


using value_type = typename std::iterator_traits::value_type ;
std::transform( first, last,
    static_cast< typename value_type (*)( value_type ) >(&f) ) ;

これには、オーバーロードされている関数群fのシグネチャを正確に把握する必要がある。例えば、fのシグネチャは以下のようになっているかもしれない。

int f( int, int dummy = 0 ) ;
double f( double ) ;

こうなると、もはや明示的なキャストを使って汎用的なコードを書くことはできない。

lambda式を使えば、汎用的なコードを書くことができる。

std::transform( first, last,
    []( auto && e ) { return f( std::forward<decltype(e)>(e) ) ; }
) ;

しかし、やはり最初の例のように書きたい。

std::transform( first, last, f ) ;

そこで、このような記述を可能にする提案。

具体的には、テンプレートの実引数推定で、実引数fがオーバーロード関数群であるid-expressionであった場合は、以下のようなlambda式の転送関数を暗黙に生成するようになる。

[]( auto && ... args )
{
    return f( std::forward< decltype(args)>(args)... ) ;
}

名前がオーバーロード関数群ではない場合、このlambda式は生成されないので、既存のコードとの互換性の問題もない。


template < typename T > void f( T ) ;

// オーバーロード関数群
void g( int ) ;
void g( double ) ;

// オーバーロード関数群ではない
void h( int ) ;

int main()
{
    f( g ) ; // lambda式が生成される
    f( h ) ; // void (*)( int )型の関数ポインターが渡される
}

また、演算子を直接渡すこともできる。

std::sort( begin(v), end(v), operator < ) ;

テンプレートの実引数推定に手を入れているので、思わぬ恩恵もある。例えば、オーバーロード関数名の変数への代入には、実は実引数推定が働くので、オーバーロード関数やオーバーロード演算子からラムダ転送式を生成して、クロージャーオブジェクトを得ることができる。

int main()
{
    auto lc = operator < ;

    lc( 1, 2 ) ; // 1 < 2
}

これは、以下のようなコードと同等だ。

int main()
{
    auto lc = []( auto && l, auto && r )
        {
            return std::forward<decltype(l)>(l) < std::forward<decltype(r)>(r) ;
        } ;

    lc( 1, 2 ) ;
}

ドワンゴ広告

ドワンゴは本物のC++プログラマーを募集しています。

採用情報|株式会社ドワンゴ

CC BY-ND 4.0: Creative Commons — Attribution-NoDerivatives 4.0 International — CC BY-ND 4.0

2 comments:

Anonymous said...

バリアント遠いですねぇ。個人的には4か5の型を入れて運用したいと思っています。
例外は大域ジャンプだと思ってる自分はあんまり使いたくないですね。実際こういう問題になるんで。
まぁ、うまくまとまってほしいです。

Anonymous said...

話半分に聞いてください。
http://ideone.com/YEAh2N
こういうコードを書きました。
自分ではだいぶんうまくかけて満足しています。
このアルゴリズムは2分法といって、0=2X+3のような片方が0になる方程式を解析するコードです。
詳しくはウィキペディアを見てください。
このアルゴリズムは、http://akita-nct.jp/yamamoto/lecture/2004/5E/test_2/summarize/html/node3.html
をみて書きました。許可も取ってませんが、もし可能だったら標準に提案することは可能ですか?
中々面白いアルゴリズムなので有用におもいますので提案してみました。
http://www.kmonos.net/nysl/
プログラムテキスト自体はNYSLでライセンスします。

どうでしょうか。