2014-11-19

2014-10-pre-Urbana mailingsのレビュー: N4183-N4189

[意味もなくPDF] N4183: Contiguous Iterators: Pointer Conversion & Type Trait

これまで、連続したストレージを指すイテレーターに対して、contiguous iteratorというカテゴリーを新たに設けようという提案があった。問題は、contiguous_iterator_tagを導入してしまうと、random_access_iterator_tagが最も下層に存在しているとみなしてハードコードしている既存のコードと相性が悪い。

そこで、今回の提案では、連続するストレージを指すイテレーターから、生のポインターを得るための方法を標準ライブラリで提案するものとなっている。

生のポインターを必要とするレガシーなC APIなどにポインターを渡したい場合に使える。

// ポインターとサイズを取る
// よくありがちレガシーなC API
void legacy_C_API( void * ptr, size_t size ) ;

template < typename ContiguousIterator >
void f( ContiguousIterator first, ContiguousIterator last )
{
    auto raw_pointer = std::pointer_from( first ) ;
    legacy_C_API( first, std::distance( first, last ) ) ;
}

内部の実装としては、pointer_fromは、内部的にdo_pointer_fromをunqalified nameで呼び出すので、ADLが発動し、それぞれのイテレーターに対応する関数がコンパイル時にADLで発見されるものとなっている。

ADL以外の実装案として、iterator_tratisのstaticメンバー関数という案が出ている。

つくづくコンセプトが欲しい。ADLを悪用するのは汚い。

[50ページもあるが余白使いすぎで無駄がありすぎるためページ数が水増しされているクソレイアウトのPDF]N4184: SIMD Types: The Vector Type & Operations

まず、このPDFに文句を言いたい。このPDFは50ページある。さぞかし長い文書なのだろうと思ったら、なるほど、長いことは長いが、実質はその半分の25ページぐらいの長さだ。なぜ50ページになっているかというと、左右半分、上下の1割ほどは余白である。極めて面積を非効率的に使っているそびえ立つクソのような固定レイアウトのPDFである。

さて、内容はというと、SIMDベクトル型としてのクラステンプレートライブラリの考察だ。かなり詳細に、初歩的なことから書かれている。

SIMD型はベクトルループよりも、並列処理に適さないデータレイアウトを書いてしまった場合が明らかなので、より並列処理しやすいデータレイアウトが書きやすくなる。

valarrayのことはすっかり忘れ去られているようだ。

歴史的経緯としては、そもそもvalarrayはベクトル型であった。当時は、ライブラリとしてベクトル演算を最適化する手法が主流であったので、valarrayもライブラリ実装による最適化を想定していた。しかし、1990年代後半から、Expression Templatesなどのテンプレートメタプログラミングを多用した最適化手法が流行り始め、valarrayは時代遅れになって放置された。誰も標準ライブラリから取り除くべきだという主張しなかったので、valarrayはそのまま残ってしまったのだ。

さて、時代は変わった。コンパイラーの最適化技術は、当時と比べてかなり進歩した。スカラー演算をまとめてベクトル演算にしたり、ループをベクトル演算にしたりといった最適化が行われるようになってきた。しかし、やはりそのような最適化には限界がある。

多くのC++コンパイラーは、Intrinsicsという、アセンブリとほぼ等しいような薄い関数としてSIMD演算を提供している。しかし、これはあまりにも特定アーキテクチャに深く結びついていて、移植性が低い。

そこで、最初からSIMDベクトル型というものがあれば、コンパイラーはベクトル演算のコードを吐く際の最適化のヒントとして使うことができる。valarrayが一周回って最新になった。

問題は、いまさらvalarrayを大幅に拡張して使うのは互換性の問題や、設計上の想定の違いから無理であろうし、やはり独立した新しいライブラリとなるべきだろう。

[同じく非効率的なレイアウトのPDF] N4185: SIMD Types: The Mask Type & Write-Masking

N4184で考察しているSIMDベクトル型の条件分岐について。

branchは、モダンなCPUには極めて重い処理である。分岐予測をミスれば、それまでの投機実行結果を破棄して処理をやり直さねばならず、100サイクル以上も無駄にする。そのため、最近の多くのアーキテクチャにはconditional moveという命令が追加されている。

また、SIMDベクトル型の比較の結果は、ひとつのbool値ではなく、boolのSIMDベクトル型で得られる。これは既存のif/for/while/switchのような条件分岐の文法では扱えない。

論文では、Maskとwrite maskingという仕組みを考察している。

[PDF] N4186: Supporting Custom Diagnostics and SFINAE

deleted定義に文字列リテラルを記述できる文法の提案。

void f() = delete("Don't use this function.") ;

int main()
{
    f() ; // ill-formed
}

コンパイラーは、この文字列リテラルを何らかの通知メッセージとして利用できる。たとえば、コンパイルエラーメッセージとして出力するなど。

なぜこの機能が提案されているかというと、SFINAEとstatic_assertを組み合わせたような機能が欲しいからだ。

static_assertは、文字列リテラルを指定できる。コンパイラーはこの文字列を、例えばコンパイルエラーメッセージで出力することができる。


template < typename T >
void f( T & x )
{
    static_assert( has_xxx<T>::value, "Template argument must has xxx.") ;
}

SFINAEは、テンプレートのインスタンス化を失敗させ、オーバーロード解決の候補から除外することによって、条件に合わない型を弾くことができる。

template < typename T >
std::enable_if_t< has_xxx<T>::value, void >
f( T & x )
{

}

問題は、SFINAEによってテンプレートのインスタンス化を阻害しても、メッセージを出力することができない。static_assertはインスタンス化しなければ働かないのだ。しかし一方で、SFINAEによる選択も行いたい。SFINAEの利点と、static_assertのメッセージの両方が欲しい。

このため、deleted定義に文字列リテラルを記述できる文法が提案されている。

template < typename T >
std::enable_if_t< !has_xxx<T>::value, void >
f( T & x ) = delete("Template argument must has xxx.")

このように、想定に合わない型をわざとdeleted定義されている関数テンプレートが最適関数になるようにしておけば、SFINAEによる選択かつコンパイルエラーかつメッセージが実現できる。

N4187: C++ Ostream Buffers

ostreamは競合しないと規定されているが、複数回の出力が分断されずに出力される保証はない。何らかの外部的な同期が必要になるが、同期方法は標準にない。標準にない場合、複数のお互いに非互換な同期処理が自作される。ostreamの同期処理は標準で存在すべきである。

この提案は、色々と設計の変遷を経たうえで、現在は、バッファー案になっている。

...
{
    std::ostream_buffer bout( std::cout ) ;
    bout << "hello, " << "World!" << std::endl ; 
}
...

このようにostream_bufferでラップして、そのオブジェクトに出力すれば、ostream_bufferのオブジェクトの寿命が来た時に、複数回の出力をまとめて、一気に出力してくれる。複数回の出力の間に他の出力がまざることはない。

N4188: Proposal for classes with runtime size

この論文は、C++に実行時にサイズが決まるクラスを提案している。

よくある実行時にサイズの変わるストレージを扱うクラスを考える。

template < typename T >
class runtime_size_buffer
{
    std::size_t size ;
    T * ptr ;
public :
    runtime_size_buffer( std::size_t size )
        : size(size), ptr(new T[size])
    { }
    ~runtime_size_buffer( )
    {
        delete[] ptr ; 
    }
} ;


auto f( std::size_t size )
{
    return std::make_unique<runtime_size_buffer>(size) ;
}

このコードは非効率的である。sizeof(runtime_size_buffer)分のストレージを確保して、次に内部的なストレージを確保している。メモリ確保が二回発生している。

しかし、最も効率的な、メモリ確保を一回しかしないコードを書くには、型システムを迂回しなければならない。

struct rsb
{
    std::size_t size ;
    char buf[1] ;
} ;


rsb * allocate_rsb( std::size_t bufsize )
{
    void * raw_ptr = std::malloc( sizeof(rsb) + bufsize -1 ) ;
    rsb * ptr = reinterpret_cast< rsb * >( raw_ptr ) ;
    ptr->size = bufsize ;
    return ptr ;
}

型システムを完全に迂回しなければならない上に、規格上未定義である。

GCC拡張では、このような現実に書かれているコードを支援するために、構造体の最後の配列の要素数を0にすることができる。

typedef struct
{
    size_t size ;
    char buf[0] ;
} zero_size_array ;

C99では、この現実に行われている手法を規格でサポートするために、Flexible Array Memberを追加した。配列の添字を記述しない文法になっている。この構造体の後に続くメモリーが、配列のアクセスでそのままアクセスできることを規格上規定している。


typedef struct
{
    size_t size ;
    char buf[] ;
} flexible_array_member ;

しかしこれは、型システムの迂回である。型があるからこそ、コンストラクターやデストラクター、virtual関数、またコードの誤りをコンパイル時に検出したりできるのだ。クラスの後に続くストレージに配列でアクセスできると仮定しても、C++にこの手法をこのまま持ち込むと、以下のような極めて醜悪なコードになってしまう。

// アライメントやパディングなどは適切に動くものとする
template < typename T >
struct rsb
{
    std::size_t size ;
    T buf[1] ;

    rsb( std::size_t size )
        : size(size) { }
} ;

// unique_ptrのためのデリーター
template < typename T >
struct rsb_deleter
{
    void operator () ( rsb<T> * ptr ) const
    {
        // 一つづつ破棄
        T * iter = std::addressof(ptr->buf[1]) ;
        T * end = iter + ptr->size -1 ;
        for ( ; iter != end ; ++iter )
        {
            iter->~T() ;
        }

        ptr->~rsb() ;
        ::operator delete( ptr ) ;
    }
} ;

template < typename T >
using unique_rsb_ptr = std::unique_ptr< rsb<T>, rsb_deleter<T> > ;

template < typename T >
unique_rsb_ptr< T > make_rsb( std::size_t bufsize )
{
    void * raw_ptr = ::operator new( sizeof( rsb<T> ) + sizeof(T) * (bufsize -1) ) ;
    // placement newでrsbを構築
    unique_rsb_ptr< T > ptr( new(raw_ptr) rsb<T>( bufsize ) ) ;

    // placement newで配列の要素を一つづつ構築
    for (
        T * iter = std::addressof( ptr->buf[1] ), * end = iter + bufsize - 1 ;
        iter != end ; ++iter )
    {
        new(iter) T ;
    }

    return ptr ;
}


struct Foo
{
    Foo() { std::cout << "constructed" << '\n' ; }
    ~Foo() { std::cout << "destructed" << '\n' ; }
} ;

int main()
{
    auto ptr = make_rsb<Foo>( 3 ) ;
}

このようなコードを書くのは面倒で、もしコードに誤りがあったとしても、コンパイル時エラーにはならない。

C++に必要なのは、型システムの恩恵をうけつつ、このような実行時にクラスのサイズを決定できる機能だ。そこでN4188が提案しているのが、実行時にサイズの決まるクラスだ。

この機能のサンプルコードは以下のようになる。

// もちろんテンプレートが使える
template < typename T >
class X
{
    const std::size_t size ;
    int other_members ;
    T buf[size] ;
public :
    X( std::size_t size )
        : size(size)
    { }
} ;

int main()
{
    // 実行時配列の要素数5
    auto p1 = std::make_unique( 5 ) ;
    sizeof( X ) ; // 配列を除くコンパイル時サイズ
    sizeof( *p1 ) ; // 実行時配列を含む実行時サイズ
}

実行時サイズ配列(runtime size array)とは、最後のデータメンバーの配列である。上の例ではbufだ。実行時サイズ配列を持つクラスを実行時サイズクラスと呼ぶ。

実行時サイズ指定子(runtime size specifier)とは、実行時サイズ配列の要素数として指定できる。上の例ではsizeだ。const修飾された整数型で、最初の非staticデータメンバーでなければならない。

実行時サイズクラス型にsizeofを適用すると、コンパイル時のサイズを返す。つまり、配列を除くサイズだ。

実行時サイズクラスのオブジェクトにsizeofを適用すると、実行時のサイズを返す。

論文では、当初の制限として、実行時サイズクラスに以下のような制限を課している。

  • 実行時サイズクラスはデータメンバーに実行時サイズオブジェクトを持てない。
  • 実行時サイズクラスはvirtual基本クラスを持てない。
  • 実行時サイズクラスが基本クラスになるときは、派生クラスの最も右側の基本クラスでなければならない。
  • 実行時サイズクラスは配列の要素型になることはできない。

論文では、後の拡張提案で、これらの制限のうち緩和できるものは緩和するとしている。

さて、コンパイラーはどうやってこの実行時サイズクラスのオブジェクトを構築するのか。通常の型Tのオブジェクトの構築は、以下のとおりである。

  1. 正しくアラインされたsizeof(T)バイトのメモリーを確保する
  2. 確保されたメモリーに対してplacement newを呼ぶ。

上記の実行時サイズクラスX<T>は、以下のように構築される。

  1. すべてのコンストラクターの実引数の値を計算する
  2. 実行時サイズ式の値を計算し、一時オブジェクト__tmp_nに格納する。
  3. __tmp_nが負数であれば、std:bad_array_length(あるいは同等のもの)を投げる。これはoperetor new[]の挙動に沿うものである。
  4. 正しくアラインされたsizeof(X<T>) + __temp_n * sizeof(T)バイトのメモリーを確保する
  5. 確保されたメモリに対してplacement newを呼び出す。実行時サイズ指定子の初期化子は、__temp_nに置き換える。

実行時サイズ指定子がクラスの最初のデータメンバーであることにより、コンパイラーはコンパイル時に、クラスの実行時のサイズを格納している場所を認識できる。

実行時サイズクラスは、動的ストレージ上にしか確保できない。

型システムの恩恵を受けつつ、C99のFlexible Array Member相当のことができる機能である。

N4189: Generic Scope Guard and RAII Wrapper for the Standard Library

汎用的なスコープガードとRAIIラッパーの提案。

前回からの変更点は、scope_guardがscope_exitに解明されたこと。

スコープガードとは、スコープを抜けた時に実行される処理のことである。例えば、関数から抜けた時に、必ずメッセージを出力したいとする。


void f()
{
    if ( !is_ok() )
    {
        std::cout << "exit function f.\n" ;
        return ;
    }

    try {
        new int ;
    } catch( ... )
    {
        std::cout << "exit function f.\n" ;
        throw ;
    }

    std::cout << "exit function f.\n" ;
    return ;
}

このようなコードを手で書くのは甚だしく面倒である。

N4189で提案されているscope_exitを使えば、以下のように書ける。

void f()
{
    auto guard = std::make_scope_exit( []{ std::cout << "exit function f.\n" ; } ) ;

    if ( !is_ok() )
        return ;

    new int ;
}

scope_exitのオブジェクトは、自動ストレージ上に構築されることを前提に設計されている。オブジェクトが破棄される際に、指定した関数オブジェクトを実行してくれる。そのため、どのような方法でスコープを抜けようとも、必ず実行される。

RAIIラッパーとは、unique_ptrをより汎用的にしたライブラリだ。

解放処理が必要な何らかのリソースがある。例えばFILE *であったり、OSやライブラリの提供するAPIであったりなどだ。これは通常、RAIIという技法を使って管理する。

struct file_resource
{
    std::FILE * res ;
    explicit file_resource( std::FILE * fp )
        : res( fp )
    { }

    file_resource( file_resource const & ) = deete ;
    file_resource( file_resource && rhs )
        : res(rhs.res)
    {
        rhs.res = nullptr ;
    }

    file_resource & operator = ( file_resource const & ) = delete ;
    file_resource & operator = ( file_resource && rhs )
    {
        if ( this != &rhs )
        {
            res = rhs.res ;
            rhs.res = nullptr ;
        }
    }
    

    ~file_resource()
    { std::fclose( fp ) ; }

    FILE * get() const { return res ; }
} ;

int main()
{
    file_resource fp( std::fopen(...) ) ;
}

このようなRAIIラッパーをいちいち自前で書くのは面倒だ。汎用的なRAIIラッパーが標準ライブラリに欲しい。N4189で提案されているunique_resourceを使えば、以下のように書ける。


int main()
{
    auto fp = std::make_unique_resource( std::fopen(...), [](auto fp){ std::fclose( fp ) ; } ) ;
    std::fputs( "hello", *fp ) ;
}

これにより、汎用的なRAIIラッパーを、わざわざ自分でクラスを書くことなく使える。

ドワンゴ広告

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

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

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

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

1 comment:

Anonymous said...

N4188に期待ですね。
まぁ、ベクタでもいいんですけど、構造体をそのまま書き出したりとかできないですからね。画像処理がはかどる気がします。気がします。
構造を愚直に表現できるようになるので。