e!doctype html> 本の虫: C++標準化委員会の文書: P0050R0-P0059R0

本の虫

著者:江添亮
ブログ: http://cpplover.blogspot.jp/
メール: boostcpp@gmail.com
Twitter: https://twitter.com/EzoeRyou
GitHub: https://github.com/EzoeRyou

アマゾンの江添のほしい物リストを著者に送るとブログ記事のネタになる

筆者にブログのネタになる品物を直接送りたい場合、住所をメールで質問してください。

C++標準化委員会の文書: P0050R0-P0059R0

[PDF] P0050R0: C++ generic match function

variantやoptionalなど、type erasureを使って複数の型を格納するsum typeは、それぞれが独自のvisitorインターフェースを提供している。例えば、std::experimantal::variantはvisitを、Boost.variantはapply_visitorを提供している。

ライブラリごとに異なる方法で要素にアクセスするのは汎用的ではない。そこで、この提案では、汎用的にtype erasureされた要素にアクセスできるmatch関数テンプレートを提案している。

matchは以下のように宣言されている。

template <class R, class F>
R match(ST const&, F&& );

戻り値の型Rがないバージョンもある。

matchはライブラリによるcustomization pointであり、ライブラリが対応させる。

たとえば、Boost.variantをmatchに対応させる場合は、以下のような実装になる。

namespace boost {
template <class R, class F, class ...Ts >
R match(variant<Ts...> const& v, F&& f)
{
    return apply_visitor(std::forward<F>(f), v); }
}

std::variantでvisitを使うには、以下のように書く。

std::experimantal::variant< int, double > a = 2 ;
std::experimental::visit( []( auto x ) { }, a ) ;

これをmatchで書き直すと、以下のようになる。

std::experimental::variant< int, double > a = 2 ;
std::experimental::match( a, []( auto x ) { } ) ;

variantのvisitがvisitorを先に書くのに対し、matchは後に書く。

また、P0051が提案するoverloadライブラリを使うと、以下のように書くこともできる。

std::experimental::match( a, overload( 
    []( int x ) { },
    []( double x ) { } ) ;

複数のsum typeに対するvisitorも書ける。

std::experimental::variant<int, X> a = 2;
std::experimental::variant<int> b = 2;
std::experimental::visit(overload(
    [](int i, int j )
    {
    },
    [](auto i, auto j )
    {
        assert(false);
    }
   ), a, b);

現在、戻り値の型の決定方法、customization pointとしてどの関数を洗濯するかmatchより既存のvisitの方がわかりやすいか、引数の順序などの議論があるらしい。

[PDF注意] P0051: C++ generic overload function

選択した関数オブジェクト群を保持する関数オブジェクトを返し、呼び出すとオーバーロード解決して最適関数を呼び出してくれるoverload関数テンプレートの提案。

auto o = overlord(
    []( int ) { },
    []( double ) { },
    []( std::string ) { } ) ;

o( 0 ) ; // int
o( 0.0 ) ; // double
o( "hello" ) ; // std::string

簡易的な実装は以下の通り。

template < typename ... Functors >
struct overloader ;

template < >
struct overloader < >
{
    void operator () () { }
} ;

template < typename Functor0, typename ... Functors >
struct overloader< Functor0, Functors ... >
    : Functor0, overloader< Functors ... >
{
    overloader( Functor0 && functor0, Functors && ... functors )
        : Functor0( std::forward<Functor0>(functor0) ),
          overloader< Functors ... >( std::forward<Functors>( functors ) ... )
    { }

    using Functor0::operator () ;
    using overloader< Functors ... >::operator () ;
} ;

template < typename ... Functors >
overloader < Functors ... >
overload( Functors && ... functors )
{
    return overloader< Functors ... > ( std::forward<Functors>(functors) ... ) ;
}

渡されたすべての関数オブジェクトから派生し、すべての関数オブジェクトのoperator ()をusing宣言で最も派生したクラスのスコープに持ち込むことにより、オーバーロード解決を実現している。

この実装では、関数オブジェクトが関数ポインターやメンバー関数ポインターの場合は動かないが、ラッパーを挟めばオーバーヘッドなしに対応できる。提案では関数ポインターやメンバー関数ポインターもサポートするようになっている。

興味本位でざっと実装したら以下のようになった。

// std::invoke
  template<typename Functor, typename... Args>
  typename std::enable_if<
    std::is_member_pointer<typename std::decay<Functor>::type>::value,
    typename std::result_of<Functor&&(Args&&...)>::type
  >::type invoke(Functor&& f, Args&&... args)
  { 
    return std::mem_fn(f)(std::forward<Args>(args)...); 
  }
   
  template<typename Functor, typename... Args>
  typename std::enable_if<
    !std::is_member_pointer<typename std::decay<Functor>::type>::value,
    typename std::result_of<Functor&&(Args&&...)>::type
  >::type invoke(Functor&& f, Args&&... args)
  { 
    return std::forward<Functor>(f)(std::forward<Args>(args)...); 
  }

  template<typename Return, typename Functor, typename... Args>
  Return invoke(Functor&& f, Args&&... args)
  {
    return invoke(std::forward<Functor>(f), std::forward<Args>(args)...);
  }

// simple callable wrapper
template < typename Functor, typename ... Args >
struct sfun_impl
{
    Functor f ;

    sfun_impl( Functor f ) : f( f ) { }

    auto operator() ( Args && ... args )
    {
        return invoke( f, std::forward<Args>(args)... ) ;
    }
} ;

// function pointer
template < typename R, typename ... Args >
sfun_impl< R (*)( Args ... ), Args ... >
sfun( R (*p)( Args ... ) )
{
    return sfun_impl< R (*)( Args ... ), Args ... >( p ) ;
}

// member function pointer with class pointer
template < typename R, typename C, typename ... Args >
sfun_impl< R (C::*)( C *, Args ...), C *, Args ... >
sfun( R (C::* p)( C *, Args ...) )
{
    return sfun_impl< R (C::*)( C *, Args... ), C *, Args ... >( p ) ;
}

// member function pointer with class reference
template < typename R, typename C, typename ... Args >
sfun_impl< R (C::*)( C &&, Args ...), C &&, Args ... >
sfun( R (C::* p)( C &&, Args ...) )
{
    return sfun_impl< R (C::*)( C &&, Args... ), C &&, Args ... >( p ) ;
}

// just to pass class type T
// lazy conditional can be used instead.
template < typename T >
void sfun( T && ) ;


template < typename T >
using sfun_if =
    typename std::conditional<
        std::is_function< typename std::remove_pointer< typename std::remove_reference<T>::type >::type >::value ||
        std::is_member_function_pointer<T>::value || 
        std::is_member_object_pointer<T>::value, 
            
        decltype(sfun( std::declval<T>() ) ), T >::type ;

// primary template
template < typename ... Functors >
struct overloader ;

// terminator
template < >
struct overloader < >
{
    void operator () () { }
} ;

template < typename Functor0, typename ... Functors >
struct overloader< Functor0, Functors ... > : sfun_if<Functor0>, overloader<Functors...>
{
    overloader( Functor0 && func0, Functors && ... funcs )
        : sfun_if<Functor0>( std::forward<Functor0>(func0) ),
          overloader<Functors ...>( std::forward<Functors>(funcs) ... )
    { }

    using sfun_if<Functor0>::operator () ;
    using overloader<Functors...>::operator () ; 
} ;



template < typename ... Functors >
overloader< Functors ... >
overload( Functors && ... functors )
{
    return overloader< Functors ... >( std::forward<Functors>(functors) ... ) ;
} 

また、提案では、登録順で呼び出し可能な最初の関数を呼び出す、first_overloadも提案している。


auto fo = first_overload (
    []( void * ) { },
    []( double ) { },
    []( int ) { } ) ;

fo( 123 ) ; // double

これは、おそらくtupleのようなものに格納した上で、is_callable(実装方法はN4446を参照)のようなメタ関数を適用するのだろう。

便利なライブラリだ。

[PDF] P0052R0: Generic Scope Guard and RAII Wrapper for the Standard Library

汎用RAIIラッパーとライブラリ版finallyの提案。

ヘッダーファイル<scope>をincludeすることで使える。

scope_exit

scope_exitは、ライブラリ版のfinallyだ。ブロックスコープを抜ける際に指定した関数オブジェクトを実行してくれる。

void f()
{
    scope_exit s( [](){/* 処理 */} ) ;

    // デストラクターが自動的に処理を実行してくれる
}

自動ストレージ上に確保する習慣をつけさせるため、make_scope_exit関数も用意されている。

unique_resource

汎用RAIIラッパー


void f()
{
    auto fp = make_unique_resource( fopen( ... ),
        []( FILE * fp )
        {
            if ( fp != NULl )
                fclose( fp ) ;
        } ) ;

        fread( ..., fp.get() ) ;

    // デストラクターがデリーターを実行してくれる
}

はやくほしい。

P0053R0: C++ Synchronized Buffered Ostream

同期バッファつきostream、osyncstreamの提案。

ストリーム出力は競合を起こさないと規定されているが、結果については規定されていない。そこで、出力をバッファして、途中に他の出力を挟まずに一括して出力できる機能の提案。


{
    std::osyncstream bout( std::cout ) ;
    bout << "hello," << "world" << std::endl ;
    // boutのデストラクターはバッファを一括して出力
}

P0054R0: Coroutines: Reports from the field

現在提案中のコルーチン、レジューム可能関数を実装しているコンパイラーがでてから丸一年になる。実際の使用経験から得られた知見をもとに、設計を見直す提案。様々な変更が提案されている。

P0055R0: On Interactions Between Coroutines and Networking Library

ネットワークライブラリの非同期処理は、futureによる継続ベースの処理と、コールバック関数による処理ができるが、コルーチンを使うとオーバーヘッドがさらに削減できるのでコルーチンに対応させる提案。

また、直接関係ないが、コルーチンは[[nodiscard]]の恩恵を受けるということが書かれていて面白い。await f()と書くべきレジューム可能関数fを、f()のみで使った場合はコンパイルエラーにできる。

P0056R0: Soft Keywords

ソフトなキーワードを導入する提案。

C++標準化員会の会議では、よく、「いいヤツは全部使われてるから、変なヤツを使わなきゃならん」という発言が行われる。ヤツというのは、キーワードのことだ。

最近の提案に含まれているキーワードを、既存のコードベースと比較するだけでも、await, concept, requires, synchronized, module, inspect, whenは、すでに変数名やフィールド名や引数名や名前空間名として使われている。

そこでこの提案では、ソフトキーワードを提案している。ソフトキーワードは、std名前空間に暗黙に定義されるエンティティである。

void yeild(int) ;

auto f()
{
    yeild( 1 ) ; // ::yeild
    std::yeild( 2 ) ; // キーワード
}

名前の衝突がない場合、using宣言を使うことでスコープ解決演算子を省略できる。

auto f()
{
    using std::yeild ;
    yeild( 3 ) ;
}

しかし、using宣言を書くのも面倒だ。提案では、ソフトキーワードに対する名前検索が何も見つけられなかった時に、ソフトキーワードだと認識するようにする提案もしている。

auto f() { yeild( 1 ) ; // std::yeild } void yeild( int ) ; auto g() { yeild( 2 ) ; // ::yeild }

ただし、この変更を受け入れた場合でも、テンプレートの場合は、依存名の解決のために、std::が必要だ。


template < typename T >
auto f( T t )
{
    yeild( 0 ) ; // 非依存名、std::yeild

    std::yeild( t ) ; // 依存名
}

typenameやtemplateと同じで、Two-Phase lookupについてはすでに知られているので、テンプレートコードを書くプログラマーならば対処できる些細な問題だろう。

ただし、using namespace stdを使った既存のコードが壊れる可能性がある。これを防ぐには、using directiveはソフトキーワードには働かない例外条項を付け加えればよいという提案もしている。

確かに便利だが、ものすごいツギハギ感がある。

[PDF] P0057R0: Wording for Coroutines (Revision 3)

コルーチンの文面案。

まだ、awaitとyeildのキーワードが固まっていない。

久しぶりに文面案を見ると、だいぶ変わっている。

まず、キーワードとして、await-keywordとyeild-keywordがある。このキーワードはプレイスホルダーで、将来的には何らかのキーワードが割り当てられる。

await式は、オペランドの式の実行が完了するまで、コルーチンの評価を停止させる。

auto read_from_disk()
{
    await read_gigabytes_of_file() ;
}

yeildキーワードは、await式を適用した後に、コルーチンのpromiseオブジェクトpに対して、p.yeild_value(yeilded value)を呼び出す。つまりシンタックスシュガーだ。yeild式は、generatorを作るのに使える。

auto gen( )
{
    for ( int i = 0 ; i < 10 ; ++i )
        yeild i ;
}

しかし、戻り値の型がよくわからない。await式のみが使われているとき、await式とyeild式が両方使われているとき、yeild式のみが使われているときで、型が変わる。どうも、その型はコルーチンpromiseを実装した型で、どうもユーザーが実装しなければならないようだ。

[PDF] P0058R0: An Interface for Abstracting Execution

様々な実行媒体を隠匿して扱えるライブラリの提案。

実行媒体には、スレッドプールを始めとして、ファイバーやGPUスレッドやSIMDなど様々なものがあるが、この提案では、それらの多種多様な実行媒体を効率的に隠匿できる設計にしていると主張している。

[PDF] P0059R0: Add rings to the Standard Library

リングアダプターの提案。既存のコンテナーをリングと呼ばれる固定サイズのキューとして扱えるコンテナアダプター。

既存のキューアダプターは、dequeやlistの上に構築されていて、動的にサイズを増加させる。これはメモリを断片化させる問題がある。リングアダプターは、arrayやvectorといった連続したストレージを使うコンテナーのアダプターで、動的にサイズを増やすことがない。

すでにBoostにはcircular_bufferがあるが、これはあまりにもでかすぎる。双方向である上にランダムアクセスイテレーターまで提供している。

インターフェースは既存のqueueを模倣している。ただし、pushとemplaceに加えて、try_pushとtry_emplaceが追加されている。これはリングが満杯の時に呼び出すと失敗する。

arrayがデフォルトのコンテナーになっているfixed_ringと、vectorがデフォルトのコンテナーになっているdynamic_ringがある。dynamic_ringという名前だが、サイズを実行時の初期化時に指定できるだけで、サイズがあとから伸長されることはない。後からサイズを増やしたければ、すでにあるqueueにvectorを指定して使えばいいだけだ。

ドワンゴ広告

明日はFallout 4が解禁されるために有給を取る。

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

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

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