2014-06-09

2014-05 pre Rapperswil mailingのレビュー: N3967-N3979

2014-05 pre Rapperswil mailingが公開された。ざっとタイトルを眺めると、今回も面白そうな論文がちらほらある。早速レビューしていこう。

N3967: C++ Standard Library Active Issues List
N3968: C++ Standard Library Defect Report List
N3969: C++ Standard Library Closed Issues List

標準ライブラリに存在が認知されている既存の問題集、修正済みの問題集、議論の結果実は問題ではなかったとされた問題集。

N3970: Technical Specification for C++ Extensions for Concurrency, Working Draft

新しい並列実行ライブラリを含むConcurrencyのTechnical Specification。歴史的にTSとして出た機能が、そのまま標準規格に入ることはないが、これを叩き台に、将来の規格入りを目指して、今後も並列実行ライブラリが議論されるだろう。内容としては、スレッドよりも高級な並列実行ライブラリである、ExecutorとShcedulerと、std::futureにis_readyとかthenとかwhen_allなどの便利なメンバー関数を追加する文面が入っている。

N3971: Concurrency TS Editor's Report, February 2014

今回のConcurrency TSに加えられた変更に対する、TS編集者の報告書。

[嫌気のするPDF] N3972: Source-Code Information Capture

リフレクションのひとつとして、ソースコード情報取得ライブラリの提案。

C++では、__LINE__とか__FILE__とか__func__といったCプリプロセッサーマクロにより、ソースコードファイルのその文脈における行番号とかファイル名とか関数名を取得することができる。

しかし、Cプリプロセッサーは醜悪である。筆者はその臭いに耐えられぬ。このような情報は、もっとまともな方法で取得できるべきだ。

そういうわけで、C++17向けに議論されているリフレクション機能のひとつとして、ソースコード情報取得ライブラリが提案されている。

N3972提案の設計では、source_context型のオブジェクトは、構築された文脈におけるソースコード情報を保持するのだそうだ。

// N3972提案
int main()
{
    std::source_context sc ;

    std::cout
        << u8"行番号: "  << sc.line_number() << '\n'
        << u8"行頭からの文字数: " << sc.column() << '\n'
        << u8"ファイル名: " << sc.file_name() << '\n'
        << u8"関数名: " << sc.function_name() << '\n' ; 
}

この情報が、どのような文字列で表現されるかは、実装定義(implementation-defined)である。

また、この提案は興味深いことに、source_contextをデフォルト実引数で初期化した場合や、クラスのデータメンバーとして初期化した場合に、その構築を呼び出した文脈の情報をキャプチャすると書いてある。つまり、以下のようになる。

// N3972提案
void f( std::source_context a = std::source_context() )
{
    std::cource_context b ; // bはこの場所の情報を補足する
}

void g()
{
    f() ; // aはこの場所の情報を補足する

    std::source_context c ;
    f( c ) ; // aはcのコピー
}

struct S
{
    S() { }
    S( std::source_context ) : d(sc) { }
    std::source_context d ;
} ;

void h()
{
    S s1 ; // s1.dはこの場所の情報を補足する

    std::source_context e ;
    S s2(e) ; // s2.dはeのコピー
}

デフォルト実引数がどの文脈で評価されるかということについて、規格上の定義はないし、今後も規定するかどうかはわからないが、source_contextの構築においては、そのように規定するということだ。

なぜ、デフォルト実引数やデータメンバーの構築について、わざわざ規定するかというと、これを使って、ロギングライブラリなどを実装できるようにするためだ。

論文で、議論で要望が高かったものの、今回の提案には含めなかった機能に、比較関数とstd::hashがある。これは、set/map/unordered_set/unordered_mapのようなコンテナーにstd::source_contextを格納する際に必要になる。そのような需要は当然あるだろう。問題は、いったいどうやってファイル名や関数名といった情報を比較すればいいのだろうか。いったい何が「等しい」のだろうか。簡単に意見の一致は見られないだろう。このことはもっと議論されるべきであるので、今回の提案には、比較関数やハッシュ関数は定義されていない。

他にも要望が高かった機能としては、ソースファイル先頭からの文字数を取得する方法がある。これは、コンパイラー実装の都合上、どの程度困難であるのかを見極めるために、今回の提案には含まれていない。

function_nameは、コンパイラー向けの関数名と、開発者向けの関数名の二つに分割すべきだという要望もあった。これは、「コンパイラー向け」とか「開発者向け」というものの定義が出来ない時、ひどく実装定義な機能であるとして、この提案には含めなかったそうだ。

これは泥臭い機能である。しかし、現実に必要とされている機能であることは確かだ。Cプリプロセッサーマクロよりもまともな方法であるので、筆者はこの提案を気に入っている。ただし、もっと議論が必要だろう。

ちなみに、std::source_contextのコンストラクターとメンバー関数はconstexprであるので、中3女子も一安心していることだろう。

[吐き気のするPDF] N3973: A Proposal to Add a Logical Const Wrapper to the Standard Library Technical Report

論理的なconst性を伝播させるのライブラリ、logical_constの提案

論理的なconst性は、クラスのメンバーを超えて受け継がれない。論文筆者によると、以下のコードは、経験あるC++erも驚くという。

// 驚くか?
struct A
{
    void bar() const
    {
        std::cout << "bar (const)" << std::endl;
    }

    void bar()
    {
        std::cout << "bar (non-const)" << std::endl;
    }
};
struct B
{
    B() : m_ptrA(std::make_unique<A>()) {}
    void foo() const
    {
        std::cout << "foo (const)" << std::endl;
        m_ptrA->bar();
    }

    void foo()
    {
        std::cout << "foo (non-const)" << std::endl;
        m_ptrA->bar();
    }

    std::unique_ptr<A> m_ptrA;
};

int main()
{
    B b;
    b.foo();
    const B const_b;
    const_b.foo();
}

実行結果は以下の通り。

foo (non-const)
bar (non-const)
foo (const)
bar (non-const)

筆者は、この挙動に驚かないし、むしろこの挙動以外であれば驚くのだが、どうやら、論文筆者によれば、この挙動は驚きらしい。たとえクラスのオブジェクトはconstであったとしても、データメンバーのポインター型はconstでない以上、何が驚きなのかわからない。

ただし、constメンバー関数を呼び出すのに、いちいち型キャストをするのは面倒だ。論文筆者は、const性を伝播させるために、->などの演算子をオーバーロードして、const版のポインターを経由して呼び出すというクラスを標準ライブラリに提案している。


struct B
{
    B();               // unchanged
    void foo() const;   // unchanged
    void foo();         // unchanged

    std::logical_const<std::unique_ptr<A>> m_ptrA;
};

これによって、const版のメンバー関数がよばれるようになり、実行結果も、以下のようになる。

foo (non-const)
bar (non-const)
foo (const)
bar (const)

constではないポインターを取り出す、cast_away_logical_constメンバー関数も用意されている。長ったらしい名前なのは、コードレビューでの発見を容易にするためだという。

まあ、あっても困らないライブラリではある。

[めまいのするPDF] N3974: Polymorphic Deleter for Unique Pointers

ゼロ原則と、ゼロ原則をサポートするためのunique_ptr拡張案。

かつて、C++には三原則があった。三原則、すなわち、コピーコンストラクター、コピー代入演算子、デストラクターのうち、どれかひとつでもユーザー提供されたのならば、残りの二つも、おそらくユーザー提供する必要があるだろうという原則である。

C++にムーブセマンティクスが導入されてから、これは五原則に変わった。三原則に加えて、ムーブコンストラクターとムーブ代入演算子が加わった。

五原則は、すでに一年前のことだ。2013-03-15のN3578で提唱されて、2014-01-01のN3839でも改定されている。

今回は、それとは別に論文著者でもあるPeter Sommerladが提唱したゼロ原則の話だ。

ゼロ原則、すなわち、何もユーザー提供する必要なないのならば、コンパイラーの生成したデフォルトのコンストラクター、代入演算子、デストラクターが動くべきという原則だ。

しかし、既存の多くのC++教科書では、このゼロ原則に反することを教えている。virtualデストラクターだ。

デストラクターがvirtualでなければ、基本クラス経由でデストラクターを呼び出した時に、正しい実行時の型のデストラクターが呼ばれない。

struct Base { } ;

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

int main( int argc, char ** argv  )
{
    std::unique_ptr<Base> p = std::make_unique<Derived>() ;
}

実行結果は、以下のようになる。

constructed

なんと、デストラクターが呼び出されていない。これは、デストラクターがvirtual関数でないためである。

ためしにvirtualを付けてみよう。


struct Base
{
    virtual ~Base() { }
} ;

こうすると、デストラクターが呼ばれる。デストラクターがvirtual関数となったので、実行時の型に応じて、基本クラスのポインターやリファレンスを経由しても、正しくデストラクターが呼ばれるようになる。

このため、既存のC++教科書は、派生する場合はかならず基本クラスのデストラクターを、たとえ空であっても、virtual関数にしようと推奨している。そのようなボイラープレートコードを自動生成するするIDEまである。

しかし、これはおかしくないだろうか。デストラクターで何もする必要がなければ、なぜわざわざ書く必要があるのだろうか。デフォルトは適切に動くべきである。

じつは、shared_ptrを使えば、デリーターがポリモーフィックに振る舞うために、非virtualデストラクターでも実行時にポリモーフィックに呼び出されてくれるのだ。


std::shared_ptr<Base> ptr = std::make_shared<Derived>() ;

しかし、shared_ptrはリファレンスカウントというコストがかかる。リファレンスカウントが必要ない場合、これはコストである。

一方、そのようなコストのかからないunique_ptrのデリーターは、ポリモーフィックではない。ポリモーフィックなデリーターはコストがかかるので、unique_ptrではデフォルトではないのは当然だ。しかし、ゼロ原則のためには、ポリモーフィックなunique_ptr用のデリーターが欲しい。そこで、そのようなデリーターの提案。

デリーター、safe_deleteは、基本クラスのデストラクターがvirtualでなくても、派生クラスのunique_ptrから基本クラスのunique_ptrに変換された場合に、実行時の型にしたがって、正しくデストラクターを呼び出してくれる。

// N3974提案
struct Base { } ;
struct Derived : Base
{
    ~Derived() { } 
} ;

int main()
{
    std::unique_safe_ptr<Base> ptr = std::make_unique_safe<Derived>() ;

    // Derived::~Derivedが呼ばれる
}

unique_safe_ptr、make_safe_ptrは、safe_deleteがデフォルトになったテンプレートエイリアスである。


template < typename T >
unique_safe_ptr = unique_ptr< T, safe_delete > ;

template <typename T, typename... Args >
unique_safe_ptr<T> make_unique_safe( Args && ... args ) ;

実は、もうひとつ、checked_deleteというデリーターも入っている。これは、基本クラスのデストラクターがvirtualではない場合に、ill-formed、つまりコンパイルエラーになるデリーターだ。ill-formedとならない場合は、従来のデリーターであるdefault_deleteと同じ挙動になる。

 // N3974提案
struct B1 { } ;
struct D1 : B1 { } ;

struct B2 { virtual ~B2 { } } ;
struct D2 : B2 { } ;

int main()
{
    // ill-formed
    std::unique_checked_ptr<B1> ptr = std::make_unique_checked<D1>() ; 

    // well-formed
    std::unique_checked_ptr<B2> ptr = std::make_unique_checked<D2>() ; 
}

論文には、リファレンス実装も含まれている。

N3975: URI - Proposed Wording (Revision 5)

URIライブラリの文面案。この提案は標準規格ではなく、TSとして発行されることを目指している。

N3976: Multidimensional bounds, index and array_view, revision 2

連続したストレージを、あたかも多次元配列であるかのように見せかけて操作できるarray_viewライブラリの文面案。

// N3976提案
int main()
{
    std::vector<float> v( 32 * 32 ) ;
    auto view = std::array_view< float, 2 >{ { 32, 32}, v } ;

    // std::index<2>
    for ( auto idx : view.bounds() )
    {
        view[ idx ] ; // アクセス
    }
}

[再開すべきではないPDF] N3977: Resumable Functions

中断可能関数(resumable function)の文面案。これは標準規格ではなく、TSとして発行されることを目指している。

文法を簡単に解説すると、中断可能関数をresumableキーワードで宣言し、途中で処理を返したいところで、awaitキーワードを使う。中断可能関数は、戻り値の型にfuture<T>かshared_future<T>を返さなければならない。中断可能関数の呼び出し元が、戻り値のfutureのオブジェクトにセットされた値を取り出そうとすると、中断可能関数の実行が再開される。

// N3977提案
// 時間のかかる処理
int slow() ;

std::future<int> f() resumable
{
    // 実行中断されて呼び出し元に帰るかも
    auto result = await slow() ;
    return result ;
}

void g()
{
    auto f = f() ; // 実行中断されているかも
    auto value = f.get() ; // 実行再開するかも
}

N3978: C++ Ostream Buffers

マルチスレッドからのストリーム出力で、結果を保証するためのバッファーの提案。規格上、ストリーム出力は競合しないことは保証されているが、その出力については保証されていない。そのため、複数のスレッドから同時に出力する場合でも、出力が細切れにならないことを保証するバッファーライブラリを提案している。

// N3978提案
void f()
{

    {
        std::ostream_buffer buf( std::cout ) ;
        buf << "hello, " << "world!" << std::endl ;
    } // bufが破棄されるタイミングで一気に出力される

}

ostream_bufferは、自動ストレージ上に構築して使う。通常のストリームのように使うとバッファーされ、、オブジェクトが破棄されるタイミングで、一気に競合せずに出力される。

[予定にないPDF] N3979: AGENDA, PL22.16 Meeting No. 63, WG21 Meeting No. 58, June 16-21, 2014 -- Rapperswil, Switzerland

スイスのRapperswilで6月16日から21日にかけて開催される国際会議の予定表。

ドワンゴ広告

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

C++WG論文集が出たので、ようやく、ドワンゴ社内で仕事をしているふりができるようになった。いや、正確にはこれも仕事ではないのだが。

今回は最初からTS発行を目指す提案が多い。これは、実用化されるまであと10年ないし20年はかかることを意味している。ドワンゴ社内で論文の概要を紹介すると興味を示すC++エンジニアはいるが、残念ながら、近い将来に使えるようにはならない機能だ。まだまだ議論と設計が必要なのだ。

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

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

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

3 comments:

Anonymous said...

array_viewに期待しています。
画像処理系で頑張ってほしいです。

Anonymous said...

江添さんのこのような地道な努力がISOや日本規格協会の販売するPDFファイルに繋がっているのですね
PDF作成に向けた活動、ご苦労様です

Anonymous said...

> N3978: C++ Ostream Buffers
デストラクタで複雑なことやらすのが流行しないといいですね。