2015-07-02

C++標準化委員会の文書 2015-04-pre-Lenexaのレビュー: N4460-N4469

N4460: LWG 2424: Atomics, mutexes and condition variables should not be trivially copyable

atomic, mutex, condition variableは、コピーができない。ただし、現行の規格の定義に従えば、trivially copyableになってしまう。

これらのクラスは、コピー代入演算子を明示的にdeleted定義されているが、それだけではtrivially copyableの条件から逃れられない。しかし、コピー代入演算子をdeleted定義すればtrivially copyableにならないと規定してしまうと、以下のようなクラスがtrivially copyableではなくなってしまう。

struct X
{
    const int val ;
} ;

このクラスは暗黙にコピー代入演算子がdeleted定義される。このクラスはtrivially copyableであるし、その挙動は変えたくない。

明示的なdeleted定義と暗黙のdeleted定義の意味を変えるという方法は、明示と暗黙の違いをできるだけなくしたいという点から、採用できない。

is_trivially_copyableとis_trivialに特殊化を記述する汚い方法はやりたくない。

論文では、規格の文面上、trivially copyableではないと記述する方法を提案している。

N4461: Static if resurrected

static ifの提案

static ifは少し前までかなり真剣に議論されていて入るかもしれないような雰囲気だったのだが、コンパイラー実装者の強い反対にあったため規格には入らなかった。

ここで提案されているstatic ifには、前回提案されていたものに比べて制約が多い。

  • ブロックスコープのみ
  • 新しいスコープを作る
  • 片方ブランチがwell-formedとなる条件部の値がどちらのブランチに対しても存在する。

GCCのRichard Smithによると、

N3329の「問題ある」部分は、

1) 新しいスコープを作らない

2) 選択されなかった方のブランチは完全に無視される(トークン列はパース可能でなくても構わない)

これは、少なくとも2つの有名なコンパイラーの実装で使われているテンプレートモデルと根本的に非互換である。

もし、static ifが(このスレで提案されているように)新しいスコープを導入し、static ifのどちらの分岐もインスタンス化可能であるならば(つまり、テンプレートのトークン列と同じ制約)、コンパイラー実装者の、俺の屍を越えて行けレベルの反対はなくなるだろう。

そのため、この論文で提案されているstatic ifは、Richard Smithの提示した制約を受け入れている。

なぜstatic ifは必要なのか。例えば、パラメーターパックを展開したいとすると、以下のようにオーバーロードを書かなければならない。

template <class T> 
void f(T&& t) 
{
    /* handle one T */
} 

template <class T, class... Rest> 
void f(T&& t, Rest&&... r) 
{
    f(t); 
    /* handle the tail */
    f(r...); // I think I have a bug here if I don't have a zero-param overload
}

これはstatic ifがあればもっと簡単に書ける。

template <class T, class... Rest> 
void f(T&& t, Rest&&... r) 
{
    /* 
      Tの処理
    */
    static_if (sizeof...(r)) {
    /*
      残りの処理
    */
        f(r...); // ゼロ引数のオーバーロードは必要ない。
    }
}

ある条件を満たすかどうかでコンパイル時に実装を切り替えるのも、とても簡単になる。現在はこう書かなければならないが、

template <class T, class... Args> 
enable_if_t<is_constructible_v<T, Args...>, unique_ptr<T>> 
make_unique(Args&&... args) 
{
    return unique_ptr<T>(new T(forward<Args>(args)...));
}  

template <class T, class... Args>  
enable_if_t<!is_constructible_v<T, Args...>, unique_ptr<T>>
make_unique(Args&&... args) 
{
    return unique_ptr<T>(new T{forward<Args>(args)...});
}

static ifがあれば、以下のように書ける。

template <class T, class... Args> 
unique_ptr<T>
make_unique(Args&&... args) 
{
    static_if (is_constructible_v<T, Args...>) {
        return unique_ptr<T>(new T(forward<Args>(args)...));
    } else {
        return unique_ptr<T>(new T{forward<Args>(args)...});
    }
}

明らかに簡単だ。

コンセプトでも問題は解決できるが、定義が分散してしまい極めて読みにくいコードになる。また著者はコンセプトとstatic ifを組み合わせるとオーバーロードを書かずにすむと主張している。

template <typename T, typename U> void f(T, U)
  requires C1<T> && (C2<U> || C3<U>)
{
    static_if (C2<U>) 
    {
    } 
    else if (C3<U>) 
    {
    }
}

なるほど、確かにこれは便利だ。

N4462: LWG 2089, Towards more perfect forwarding

何らかの簡単なコンパイル時ディスパッチの必要性を訴える論文。

make_unique, make_shared, allocator::constructといったファクトリー関数は、アグリゲートをうまく扱うことができない。

struct X { int a, b, c ; } ;

int main()
{
    // OK、アグリゲート初期化
    std::unique_ptr<X> p1( new X{1,2,3} ) ;

    // エラー、呼び出し可能なコンストラクターがない。
    auto p2 = std::make_unique<X>( 1, 2, 3 ) ;
}

これはなぜかというと、allocator::constructが、以下のようになっているためだ。

template < typename U, typename ... Args >
void allocator::construct(U * p, Args && ... args ) 
{
    return new( static_cast<void *>(p) ) T( std::forward<Args>(args)... ) ;
}

このままではアグリゲート初期化できない。しかし、一律リスト初期化{}にするのも問題だ。

LWG2089では、以下のような修正を提案している。

  • もし、is_constructible_v<TargetType, Args...>がtrueであれば、直接非リスト初期化を行う。
  • そうでなければ、初期化リストを使う。

これにより、最初の例が動くようになる。

この変更は、ライブラリでアグリゲートが使えるようになるし、実行時オーバーヘッドもないし、既存のコードもほとんど壊さないだろうし、ビルド時間もさほど増えないだろうし、実装は簡単だし、言語側での変更も必要ない。

要するに、この変更は望ましいものであって、さっさと規格入り作業を粛々と進めろとしか言う他ない。

コンパイラーベンダーが標準ライブラリにこれを実装することは慣れているので簡単だ。だがしかし、普通のユーザーが同じことをしたいとしたらどうするだろうか。

[超怖い話BEGIN] それには、非型boolテンプレート仮引数を取るクラステンプレートのstaticメンバー関数テンプレートにデリゲートし、クラステンプレートをfalseの場合に対して特殊化し、is_constructibleの結果によってディスパッチさせる[超怖い話END]

// 超怖い話の実装
template < bool >
struct construct_impl
{
    template < typename U, typename ... Args >
    static auto invoke( U * p, Args && ... args )
    {
         return new( static_cast<void *>(p) ) U( std::forward<Args>(args)... ) ;
    }
} ;


template < >
struct construct_impl<false>
{
    template < typename U, typename ... Args >
    static auto invoke( U * p, Args && ... args )
    {
         return new( static_cast<void *>(p) ) U{ std::forward<Args>(args)... } ;
    }

} ;

template < typename U, typename ... Args >
auto construct(U * p, Args && ... args ) 
{
    return construct_impl< std::is_constructible< U, Args ... >::value >::invoke( p, std::forward<Args>(args)... ) ;
}

あるいは、[超怖い話BEGIN] true_typeかfalse_typeかでタグ付けしたオーバーロードでディスパッチする[超怖い話END]

// 超怖い話の実装
template < typename U, typename ... Args >
auto construct_impl( std::true_type, U * p, Args && ... args )
{
     return new( static_cast<void *>(p) ) U( std::forward<Args>(args)... ) ;
}

template < typename U, typename ... Args >
auto construct_impl( std::false_type, U * p, Args && ... args )
{
     return new( static_cast<void *>(p) ) U{std::forward<Args>(args)... } ;
}

template < typename U, typename ... Args >
auto construct( U * p, Args && ... args )
{
    return construct_impl( typename std::is_constructible< U, Args ...>::type{}, p, std::forward<Args>(args)... ) ;
}

超怖い話は、そのままコードに落とせば動くぐらい、メタプログラミングにおけるコンパイル分岐の手法を簡潔にまとめている。さて、ユーザーも同じことをしたくなった時のために、この手法を教育しなければならないのだろうか。この手法は一般人が書けるだろうか? 書けないものはC++プログラマー失格なのだろうか?

論文筆者は、我々には何らかのコンパイル時ディスパッチを簡単にする方法が必要であると提案している。

static ifが入れば簡単に書けるようになりそうだ。

[PDF注意] N4463: IO device requirements for C++

イテレーター要件とかコンテナー要件などのように、IOデバイス要件を定める提案。

デバイスとの入出力の方法、デバイスの設定可能な項目の取得、デバイスの設定状態の取得と変更、デバイスの機能一覧の取得などが行える。

具体的にサポートするデバイスもないのにそんな要件だけ定めてなにか意味があるのだろうか。

[PDF注意] N4464: Pi-calculus syntax for C++ executors

π-calculusをC++で実現した論文。λ計算がシーケンシャルな処理をすべて記述できる計算力を持っているように、π-calculusも並列処理をすべて記述できる計算力を持っている

だが、誰が使うんだ? なんでC++標準化委員会の文書として公開されているのか理解できない。コンピューターサイエンスの理論としては興味深いものがあるだろうが、π計算の演算子をC++に持ち込んでも、まったく実用的だとは思えない。

N4465: A Module System for C++ (Revision 3)

モジュールの提案。

#includeの代替機能。プリプロセッサーはなくなるのではなくて共存する。

[PDF注意] N4466: Wording for Modules

モジュールの文面案

提案されているモジュールは、新しいキーワードとしてimportとmoduleを追加する。

ある翻訳単位をモジュールとするには、モジュール宣言を記述する必要がある。

module module-name ;

と記述する。module-nameは、識別子か、"モジュール名 . 識別子"となる。これは、std.vectorとかstd.stringとか、lib.math.random.knuthのような名前空間に似たモジュール名の分類を可能にする。

モジュールの中のエンティティを外部から使うには、import宣言しなければならない。

import std.string
import std.iostream

int main()
{
    std::string buf ;
    std::cin >> buf ;
}

モジュールとなる翻訳単位の中のエンティティは、明示的にexportしない限り外部には見えない。

module mylib

export void my_public_function() ;
void my_private_function() ;


// 囲むこともできる
export { ... }

mylibをimportすると、my_public_functionのみが見える。my_private_functionは見えない。別の翻訳単位でmy_private_functionという名前の関数を定義しても、別々の定義であり、ODR違反にならない。

module宣言で翻訳単位をモジュールにする。export宣言で外部に出したいエンティティをマーク。import宣言でモジュールを使う。というだけだ。

[PDF注意] N4468: On Quantifying Memory-Allocation Strategies

グローバルあロケーターに対してローカルなアロケーターはどのような条件でどのようなアロケーター戦略を使えばどのように効率的になるのかということを考察した論文。

なぜかドナルドの絵が引用されている。

N4469: Template Argument Type Deduction

非型テンプレートパラメーターの値を取るときに型も推定する機能を追加する提案。

現行では、以下のようなコードを書かなければならない。

template < typename T, T v > struct S ;

S< decltype(x), x > s ;

テンプレートパラメーターとして何らかの型の値を取りたいということは、まず型引数を得た上で、その型の値を取らなければならない。そのようなテンプレート使う側は、まず型を渡して、その後に値を渡さなければならない。ある値xがある場合は、decltypeでその型を得るのが最も手っ取り早い。

しかし、これは明らかに面倒だ。以下のように書きたい。

S<x> s ;

コンパイラーはxの型を推定できるのであるから、推定して欲しい。

さて、この機能をどのように実現するかについて、文法的に意見が分かれている。

最も簡単なものは、usingキーワードを使う文法だ。

// Tは推定される
template < using typename T, T v > struct S ;

このようにusing typenameと書くだけで、Tは推定される。

この文法の問題点は、仮引数と実引数の順序がずれるということだ。

template < using typename T, T v, using typename U, U w > struct S ;

S< x, y > s ;

そのため、using typenameは外に出す文法案もある。

template
    using typename T, typename U
    < T v, U w >
    struct S ;

これはテンプレート仮引数とテンプレート実引数の位置関係が対応する。

他にも、autoを使う文法案がある。

template < auto v > struct S ;

using typenameを使うほうが柔軟な型指定ができるが、autoの方が手軽だ。

どちらの文法案も競合しないので、両方入れるという案もあるそうだ。

ドワンゴ広告

社内で挙動が厳格に定義されている移植性に優れたuniform_int_distributionがほしいという声を聞いた。需要はあるのだろうか。

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

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

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

3 comments:

Anonymous said...

C/C++というのは基本的なターゲットコンピュータがないことが売りだったはずです、デバイスの仕様を定めるくらいなら、各種エンターテインメントファイルズの標準フォーマットを決めるほうが有意義に思います。
音とか絵とか決めたらライブラリも書きやすいと思いますし、あとは変換関数次第という話になるので簡単かと思います。
議論も活発にされていますし、昨今の事情なら時代遅れが発生することも少ないでしょう。カイロまだですか。
モジュールも早く入ってほしいです。ソースを分けて書くなんてもうできません。主にテンプレートが悪いです。
ついでにモジュールのコンパイル方法などを規定してexport復活とかもできないかなーとか思ったりしますが、まぁ、それはそれ。

Anonymous said...

「テンプレートパラメーターとして何らかの型の値を取りたいということは、まず型引数を得た上で、その型の値を取らなければならない。」

の部分からfont-familyがmonospaceになっていますので修正お願いします。

Anonymous said...

N4469の所のコード例の中でcodeタグ閉じ忘れてる。