2014-11-05

2014-10-pre-Urbana mailingsのレビュー: N4160-N4169

N4160: Value constraints

C++に、値に対する制約を記述するための機能を追加する具体的な文法案などはいくつか提案されている。この論文では、具体的な提案ではなく、値の制約ということに対して、必要な機能と実装上の課題などを考察している。

値の制約という機能は、値に対して何らかの無効であり本来起こりえない条件を指定するものである。

たとえば、ある関数はdouble型の引数をとるが、負数が与えられることは想定していないとかだ。古典的なassertの中に書かれる式も制約である。論文では他にも、同一の表現を指定するなどのことをあげている。たとえばstd::vector vのv.empty()とv.size() == 0は同じ意味であるなど。

論文は実行時のチェックよりも、コンパイル次の静的チェックに重きをおいていて、実行時チェックを自動生成するなどの機能も、Eiffelを例にあげて説明している。

N4161: Uniform Container Erasure (Revision 1)

コンテナーを引数に取り、特定の要素を効率的に削除する、erase_if( container, pred )とerase( container, value )の提案。

int main()
{
    std::list<int> l = { 1, 2, 3, 4, 5, 6, 7, 8, 9 } ;

    // 3を取り除く
    std::experimental::erase( l, 3 ) ;
    // 5未満を取り除く
    std::experimental::erase_if( l, [](auto x){ return x < 5 ; } ) ;
}

N4162: Atomic Smart Pointers, rev. 1

モダンなC++のコードにおいては、生のポインターを直接使ったり、deleteを直接呼び出したりするのを避けるべきである。unique_ptrやshared_ptrを使うべきである。これにより、リークフリーでメモリーセーフなコードを記述できる。オブジェクトの寿命が構造化されていなかったり、そもそもいつまで生きているのか分からなかったりする場合は、重要である。特に、並列処理では重宝する。

残念ながら、ロックフリーなコードを書く場合、今だに生のポインターを扱わなければならない。unique_ptr, shared_ptr, weak_ptrは、アトミックな操作をサポートしていないためである。shared_ptrだけは多少のサポートがあるが、貧弱である。

この論文は、atomic_shared_ptr<T>, atomic_eak_ptr<T>, atomic_unique_ptr<T>を提案している。

前回の論文では、atomic<*_ptr<T>>というatomicテンプレートのスマートポインター型に対する特殊化を特別に定義するという珍妙な設計だったが、会議で否定されたために、独自のテンプレートとなった。

N4163: Agenda and Meeting Notice for WG21 Telecon Meeting

2014年10月24日に行われる電話会議の予定表。

N4164: Forwarding References

&&は、具体的な型に修飾する場合と、テンプレート仮引数に就職する場合とで、意味が異なる。

struct X { } ;

void f( X && ) ;

template < typename T >
void g( T && ) ;

この例で、X &&と、T &&は、受け取れる値が異なる。

X &&はX型のnon-const non-volatileなrvaluesしか受け取れない。

T &&は、CV修飾子の有無はおろか、lvalueさえ受け取ることができる。

auto &&にも同じ性質がある。

テンプレート仮引数やautoに&&を使った場合のこの何でも受け取れるという性質の挙動については、従来、公式に名前が付けられていなかった。

しかし、この概念を教育したり議論したりする際に、名前がないと困る。そこで、この論文では、この性質をForwarding Referenceと呼ぶことを提案している。同時に、標準規格の文面上でもそのように名付ける変更案を提案している。

単に既存のプログラム上の意味に名前を付けるだけで、意味が変わるわけではない。

当初、Scott Meyersがこの性質に名前を付ける必要性を感じ、発表でUniversal Referenceと呼んだが、Universalという名前よりは、Forwardingの方がふさわしいと判断された。これは、どんな値でも転送する性質を持つからだ。

N4165: Unified Call Syntax

N4174: Call syntax: x.f(y) vs. f(x,y)

本の虫: C++に提案されている統一関数呼び出し文法(Unified Call Syntax): N4165, N4174を参照。

N4166: Movable initializer lists

std::initializer_listの所有する値には、constなポインターを経由してアクセスする。constであるがために、値を変更したり、ムーブしたりできない。

void f( std::initializer_list<int> list )
{
    // elemの型はint const &
    for ( auto && elem : list )
    {
        elem = 0 ; // エラー
    }
}

void g( std::initializer_list< std::unique_ptr<int> > list )
{
    std::vector< std::unique_ptr<int> > v ;

    // elemの型はstd::unique_ptr<int> const &
    for ( auto && elem : list )
    {
        v.push_back( std::move( elem ) )  ; // エラー
    }
}

しかし、初期化リストの初期化子がすべてリテラルででもない限り、initializer_listは、実装の詳細としては実行時にストレージを所有している。所有している以上、所有権の移動ができるはずである。しかし、現状ではconst_castを用いて、無理やりconst性を消し去る以外に、所有権を横取りする方法がない。

void f( std::initializer_list<int> list )
{
    // elemの型はint const &
    for ( auto && elem : list )
    {
        const_cast<int &>(elem) = 0 ; 
    }
}

void g( std::initializer_list< std::unique_ptr<int> > list )
{
    std::vector< std::unique_ptr<int> > v ;

    // elemの型はstd::unique_ptr<int> const &
    for ( auto && elem : list )
    {
        v.push_back( std::move( const_cast< std::unique_ptr<int> & >(elem) ) )  ;
    }
}

なぜinitializer_listがムーブセマンティクスに対応していないかというと、initializer_list設計当時の2005年から2007年は、まだムーブセマンティクスが一般的ではなく、設計に含めることができなかったためだ。2008年に、initializer_listをムーブセマンティクスに対応させるための提案もN2801として出たものの、時間が足りず、そのまま打ち捨てられた。

この論文では、initializer_listをムーブセマンティクスに対応させるための特殊化、std::initializer_list<T &&>を提案している。rvalueリファレンスは、単にオーバーロード解決でムーブ可能なinitializer_listを受け取るためのフラグであって、それ以外の意味はない。イテレーターは非constなリファレンスを返すので、以下のように書ける。

void f( std::initializer_list<int && > list )
{
    // elemの型はint &
    for ( auto && elem : list )
    {
        elem = 0 ; // OK
    }
}

void g( std::initializer_list< std::unique_ptr<int> && > list )
{
    std::vector< std::unique_ptr<int> > v ;

    // elemの型はstd::unique_ptr<int> &
    for ( auto && elem : list )
    {
        v.push_back( std::move( elem ) )  ; // OK
    }
}

従来のinitializer_list<T>の挙動は変わらない。

movable_initializer_list<T>のような別のプライマリーテンプレートを使わなかった理由としては、T &&は十分にわかりやすいし、initializer_listはよく知られている名前であるし、また、テンプレートメタプログラミングによって切り替え可能になるからだという。

内部的には、initializer_list<T && >はinitializer_list<T>から派生している。

N4167: Transform Reduce, an Additional Algorithm for C++ Extensions for Parallelism

並列実行版アルゴリズムに、transform_reduceの追加をする提案。

ドット積の計算を考える。従来のシリアル実行ならば、std::accumulateを使うことで、以下のように書ける。

struct Point {
    double x, y;
};

std::vector<Point> values(10007, Point{2.0, 2.0});

double result =
    std::accumulate(std::begin(values), std::end(values), 0.0,
        [](double result, Point curr)
        {
            return result + curr.x * curr.y;
        });

しかし、これは並列化できない。処理が進むためには、まず前回のresultが計算されなければならないからだ。

この問題を解決するには、並列版アルゴリズムの提案に含まれているreduceを使うことができる。ただし、reduceの制約上、極めて不便なworkaroundを余儀なくされる。

Point result =
    std::experimental::parallel::reduce(
        std::experimental::parallel::par,
        std::begin(values),
        std::end(values),
        Point{0.0, 0.0},
        [](Point res, Point curr)
        {
            return Point{
                res.x * res.y + curr.x * curr.y, 1.0};
        }
    );

reduceの戻り値の型は、イテレーターの値の型でなければならない。そのため、本来ならdoubleを使うべきところだが、Pointクラスのxメンバーだけを使っている。しかも、無理やり行列的な計算をしている。値をそのままにしておくためだけに、本来不必要な1.0での乗算を行わなければならない。

そこで、N4167は、イテレーターの値の方から戻り地の型へ変換する関数オブジェクトを引数に取る、transform_reduceを提案している。

double result =
    std::experimental::parallel::transform_reduce(
        std::experimental::parallel::par,
        std::begin(values),
        std::end(values),
        0.0,
        std::plus(),
        [](Point r)
        {
            return r.x * r.y;
        }
    );

N4168: Removing auto_ptr

auto_ptrを規格から削除する提案。

すでにauto_ptrはdeprecated扱いで、Annex D互換機能に移されているが、この提案で完全に削除する。

auto_ptrは、もはや使うべきではない。auto_ptrを使う既存のコードは、速やかにunique_ptrに書き変えるべきである。

N4169: A proposal to add invoke function template (Revision 1)

std::funcitonやstd::bindなどで使われている、INVOKE(§20.9.2)通りに呼び出しをしてくれる関数テンプレートinvokeを標準ライブラリに追加する提案。

std::functionを実装してみた人間ならばわかると思うが、INVOKEの仕様は結構複雑だ。

struct X
{
    int data ;
    int f() { return 0 ; }
} ;

int f() { return 0 ; }


int main()
{
    // まあ、よくある関数呼び出し
    std::function< int () > f1 = &f ;
    f1() ;

    // メンバー関数呼び出し
    std::function< int ( X & ) > f2 = &X::f ;
    X x ;
    f2( x ) ;

    // メンバー関数呼び出しのポインター版
    std::function< int ( X * ) > f3 = &X::f ;
    f3( &x ) ;

    // データメンバーへのアクセス
    std::function< int & ( X & ) > f4 = &X::data ;
    f4( x ) = 123 ;

    // データメンバーへのアクセスのポインター版
    std::function< int & ( X * ) > f5 = &X::data ;
    f5( &x ) = 123 ;
}

std::functionは、実は、データメンバーへのアクセスまで関数呼び出し風の文法でサポートしていたのをご存知だろうか。INVOKEの仕様に従っているためである。

この挙動には、SFINAEを利用したメタプログラミングが必要である。これはよく使うので、標準ライブラリにほしい。そこで、そのような挙動をする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)...); 
  }

これはなぜ標準ライブラリにないのか不思議だった。

ドワンゴ広告

この記事はドワンゴ勤務中に書かれた。

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

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

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

1 comment:

Anonymous said...

一点だけ。
N4161のイレーズという名前は紛らわしいのでフィルターという名前が個人的には好ましいです。
そこだけ気になりました。