2014-12-03

2014-10-pre-Urbanaのレビュー: N4230-N4239

N4230: Nested namespace definition (revision 2)

namespace A::B::C {
    // ...
}

を、

namespace A {
    namespace B {
        namespace C {
            // ...
        }
    }
}

と同等にする提案。

これは個人的に入るべき小粒な機能だと思う。

N4231: Terms and definitions related to "threads"

規格における「スレッド」の意味するところに意見の不一致が見られるので、スレッドの意味を明確に定義する。また、スレッドより軽い実行媒体についても定義する。

N4232: Stackful Coroutines and Stackless Resumable Functions

コルーチンとレジューム可能関数には似通った部分があるから統一してはどうかという意見に対し、コルーチンとレジューム可能関数の違いを解説している文書。

N3985が提案しているコルーチンは、ライブラリである。スタックフルな実装であり、アドレス空間を多大に消費する。ライブラリなので今すぐに使うことができる。

N3858, N3977の提案しているレジューム可能関数は、コア言語機能である。スタックレスな実装であり、コルーチンのようにアドレス空間を浪費しない。コンパイラーによるサポートが必要である。

N4233: A Class for Status and Optional Value

N3533で提案されているConcurrent Queuesライブラリでは、値と状態を別々に返すように設計されている。

Value queue::value_pop();
queue_op_status queue::try_pop(Value&);
queue_op_status queue::nonblocking_pop(Value&);

この設計には、以下の制約がある。

  • value_popで状態が悪い場合、例外が投げられる
  • try_popとnonblocking_popを使う場合、Valueがデフォルト構築可能でなければならない

なにか状態と値を同時に返せるようなライブラリが欲しい。そう、例えば以下のような。


something<queue_op_status, Value> queue::value_pop();
something<queue_op_status, Value> queue::try_pop();
something<queue_op_status, Value> queue::nonblocking_pop();

設計上の要件としては、このsomethingは、必ず状態を有するが、値を有するかどうかは分からない。存在しない値にアクセスしようとすると例外を投げる。

これは提案中のoptionalやexpectedに似ている。ただし、optionalとexpectedは、値かエラーかのどちらか片方を格納するライブラリである。ここで欲しいのは、必ず状態を格納し、値を格納しているかどうかは分からないライブラリだ。

N4233は、このような要求を満たす、status_value< Status, Value >というライブラリを提案している。

N4234: 0-overhead-principle violations in exception handling - part 2

ゼロオーバーヘッドの原則(Zero Overhead Principal)とは、使わない機能のオーバーヘッドを支払うべきではないという原則である。ある機能があったとして、その機能を使わない場合、その機能をサポートするために必要なオーバーヘッドがもたらされてはならない。C++は伝統的に、この原則を強く意識してきた。Bjarne StroustrupがSimulaを使ってシミュレーターを書いたら、使いもしないGCのオーバーヘッドでプログラムの実行時間の80%が浪費されていたことは、あまりにも有名だ。

C++で、特にオーバーヘッドのやり玉に上げられるのは、例外である。例外のオーバーヘッドについては、N4049で考察されている。組み込みなどの資源制約の厳しい環境では、例外のオーバーヘッドは高く付きすぎる。

この論文、N4234では、もしユーザーが例外を使わずにSTLを用いた場合、規格と現在の実装は、現在の最適化技術であWhole Program AnalysisやLink Time Optimizationを用いて、例外によるゼロオーバーヘッドの原則(生成されたバイナリに例外サポートのためのコードが一切含まれないことを指す)を尊重できるのかどうかを考察している。

論文は、例外を使わないコンパイラーフラグや、例外を使わないSTL実装を使うことについては考察していない。

例外処理サポートのない特別なSTL実装の使用(gccの-fno-exceptionsフラグなど)は考察しない。例外処理は言語の一部であり、言語のサブセットを定義することは、過去にEmbedded C++があったように、好ましいことではない。

この論文では、以下のようなプログラムで実験を行った。

  • ::operator new(nothrow)を呼ぶアロケーター
  • main関数
  • noexceptではないvector::push_backの呼び出し
  • 純粋仮想関数が誤って呼ばれた場合に呼ばれる関数(__cxa_pure_virtual)を、ツールチェインの例外を使うデフォルト実装を上書きして定義する(ドキュメント化された方法である)

ターゲットプラットフォームはLinux x86-64(Ubuntu 14.04)で、GNUツールチェインが用いられた。Clangでも試したらしいが、同じ結果だったそうだ。

結果としては、生成されたバイナリには、例外サポートのためのシンボルが含まれていた。

理由はいくつかある。STL実装の内部で、_M_check_lenという関数が呼ばれ、これがlength_error例外を投げている。範囲外かどうかのチェックは規格で必須になっているため、これは規格準拠の実装である。operator new(nothrow)は、例外を使ってnew_handlerから投げられる例外を確認している。

_GLIBCXX_WEAK_DEFINITION void * 
operator new (std::size_t sz, const std::nothrow_t&) _GLIBCXX_USE_NOEXCEPT 
{ 
  void *p; 
 
  /* malloc (0) is unpredictable; avoid it.  */ 
  if (sz == 0) 
    sz = 1; 
 
  while (__builtin_expect ((p = malloc (sz)) == 0, false)) 
    { 
      new_handler handler = std::get_new_handler (); 
      if (! handler) 
        return 0; 
      __try 
{ 
  handler (); 
} 
      __catch(const bad_alloc&) 
{ 
  return 0; 
} 
    } 
 
  return p; 
} 

なぜこんなコードでなければならないかというと、規格でoperator new(size)を呼び出すようになっているからだ。そして、operator new(size)は以下のように定義されている。

  • ループを実行する。ループの中で、まず要求されたストレージの確保を試みる。ストレージ確保の方法は実装依存である。
  • ストレージの確保が成功したならば、ストレージへのポインターを返す。ストレージの確保が成功しなかった場合で、現在のnew_handlerがnullポインターの場合、std::bad_allocをthrowする。
  • 現在のnew_handlerがnullポインター以外の場合、現在のnew_handlerを呼び出す。呼び出しが返ったならば、ループを続行する。
  • ループはストレージの確保が成功するか、new_handlerの呼び出しが返らなくなるまで、続けられる。

私が書いたC++11の文法と機能も参照。

operator newは、ストレージの確保に失敗した場合、new_handlerを呼び出してから、再びストレージ確保を試みる。new_handlerがループを打ち切ると判断したかどうかは、例外を使わなければ確認できない。

アロケーターでoperator new(nothrow_t)のかわりに、mallocを使ったプログラムの場合、例外サポートのためのシンボルを含まないバイナリの生成に成功したという。

このテスト結果を受けて、論文では、規格に以下のような改善案を提示している。

STLに対する改善案としては、

コンテナーにnoexcept版のメンバー関数を追加する。

引数の違いで追加する方法

void push_back (const value_type& val,  nothrow_t ) noexcept; 
void push_back (value_type&& val,  nothrow_t ) noexcept;  

あるいは別名で追加する。

範囲外チェックをoptionalにする。

C++に対する改善案としては、

noexcept修飾子で、ポインターを修飾することを可能にする。これによってオーバーロード解決でnoexcept版のメンバー関数が呼ばれる。

論文にサンプルコードはないが、以下のような感じだろうか。

struct X
{
    void f() ; // エラーを例外で通知
    bool f() noexcept ; // エラーを戻り値で通知
} ;

int main()
{
    X noexcept x ;
    x.f() ; // noexceptのオーバーロードが呼ばれる。
}

うーむ・・・

new_handlerに対しては、例外を投げないnew_nothrow_handlerを追加する改善案が示されている。s

標準ライブラリに例外を投げないstd::nothrow_allocatorを追加する案も示している。

N4235: Selecting from Parameter Packs

テンプレートパラメーターパックを扱う際に、N番目の型を取り出したいことがよくある。これは以下のように書くことができる。

template<int N, typename T0, typename ... Tr> struct nth_type_impl {
  using type = typename nth_type_impl<N-1, Tr...>::type;
};
  template<typename T0, typename ... Tr>
  struct nth_type_impl<0, T0, Tr...> {
    using type = T0;
  };
template<int N, typename ... Ts>
using nth_type = typename nth_type_impl<N, Ts...>::type;

template<typename ... Ts> struct App {
  nth_type<3, Ts...> n1;  // 4番目の要素.
}

これは動く。動くのだが、問題がある。これは再帰的なテンプレートのため、テンプレートのインスタンス化が大量に発生し、コンパイラー資源の浪費である。また、エラーメッセージもわかりにくい。

N4235では、パラメーターパックを扱うための便利な文法をいくつか提案している。

パラメーターパックからN番目の要素を取り出す文法

先ほどのAppは、以下のように簡潔に書くことができる。


template < typename ... Ts >
struct App
{
    Ts.[3] n1 ; // 4番目の要素
} ;

存在しない場合はエラーになる。

template < typename ... Types >
struct first
{
    using type = Types.[0] ;
} ;

using type = first<>::type ; // ill-formed

パックサブセット、パラメーターパックTsからsizeof...(Ns)だけ切り出す文法

Ts.<Ns...>

先頭からsizeof...(Ns)だけ要素を切り出したパラメーターパックになる。

N番目の要素だけ指定して切り出す文法

Ts.<1, 3, 5>

これは、2番目と4番目と6番目の要素のパラメーターパックになる。

Ts.<0, Ns..., 0>

これは、前後を最初の要素に挟まれたTs.<Ns...>になる。

整数シーケンスのパック展開を簡単に作れる文法 b ... < eも提案されている。

// {1,2,3,4,5,6,7,8,9}
int digits = { 0 ... < 10 } ;

これは、パックサブセットと組み合わせると強力になる。

// 最初の要素を除いたパラメーターパック
Ts.< 1 ... < sizeof...(Ts) >
// 最後の要素を除いたパラメーターパック
Ts.< 0 ... < sizeof...(Ts) - 1 >
// 要素を二度繰り返したパラメーターパック
Ts.< 0 ... < sizeof...(Ts), 0 ... sizeof...(Ts) >

また、この文法をプリプロセッサーでサポートするためにpp-numberにも変更を加える提案をしている。

面白い機能だ。文法にバイク小屋議論はあるだろうが、このような機能は欲しい。

N4236: A compile-time string library template with UDL operator templates

C++の文字列リテラルは、C言語から受け継いだ。C++では、std::basic_stringによって、文字列操作を高級にした。しかし、まだC++における文字列は扱いづらい。論文では、以下のようなサンプルコードで例示している。


// 結合
  //
  auto x1 = "Hello" + ", " + "World!"; // エラー!
  auto x2 = std::string("Hello") + ", " + "World!"; // 動くけど汚い。あと実行時コストがかかる
  auto x3 = "Hello" + std::string(", ") + "World!"; // エラー!
  auto x4 = "Hello" ", " "World!"; // 動くけど・・けど・・・

  auto conjunction = std::string(", ");
  auto x5 = "Hello" conjuction "World!"; // けど、これはエラーだろ。

  // ジェネリックプログラミング
  //
  template <typename T>
  typename T::size_type find(T t, typename T::value_type q)
  {
      return t.find(q);
  }

  auto s1 = std::string("One");
  auto s2 = "Two";

  find(s1, 'n'); // これはいい
  find(s2, 'w'); // エラー!
  find(std::string(s2), 'w'); // うごくけど、ちょっと待てよオイ

  // テンプレートメタプログラミング
  //
  template <typename T> struct metaprogram { /* ... */ };

  metaprogram<"Hello, World!"> m1; // エラー!
  metaprogram<std::string("Hello, World!")> m2; // そもそもなんじゃこりゃ
  metaprogram<decltype("Hello, World!")> m3; // そういう意味じゃない
  metaprogram<decltype(std::string("Hello, World!"))> m4; // これも違う
  metaprogram<boost::mpl::string<'Hell','o, W','orld', '!'>> m5; // やったぜ! ってなめとんのかー!
  metaprogram<_S("Hello, World!")> m6; // 詳細は気にするな

この論文は、文字列リテラルを使いやすくするために、新しいテンプレートライブラリstd::basic_string_literalと、ユーザー定義リテラルを提案している。これにより、上のコードは、以下のように書ける。

  // 結合
  //
  constexpr auto x1 = "Hello"S + ", "S + "World!"S; // 動く。実行時コストなし

  constexpr auto conjunction = ", "S;
  constexpr auto x5 = "Hello"S + conjuction + "World!"S; // 動く、実行時コストなし。

  // ジェネリックプログラミング
  //
  template <typename T>
  constexpr typename T::size_type find(T t, typename T::value_type q)
  {
      return t.find(q);
  }

  auto s1 = std::string("One");
  constexpr auto s2 = "Two"S;

  find(s1, 'n'); // 動く。
  find(s2, 'w'); // 動く。実行時コストなし

  // Template Metaprogramming
  //
  template <typename T> struct metaprogram { /* ... */ };

  metaprogram<decltype("Hello, World!"S)> m1; // 動く、実行時コストなし

このライブラリ、std::basic_string_literalのコンストラクターはprivateである。std::basic_string_literalは、ユーザー定義リテラルoperator "" Sによってのみ構築することができる。実行時オブジェクトを誤って作り出してしまうことを防ぐためである。std::basic_string_literalは、std::basic_stringにインターフェースを似せて設計されており、const std::basic_string相当のことができるようになっている。

N4237: Language Extensions for Vector loop level parallelism

Clik PlusやOpenMP 4.0を土台にしたベクトルループをコア言語機能として追加する提案。プリプロセッサーではなくキーワードを使う。

int main()
{
    int a[100] = { ... };
    int b[100] = { ... };
    int c[100] = { ... };

    for simd ( int i = 0 ; i < 100 ; ++i )
    {
        c[i] = a[i] + b[i] ;
    }

}

ベクトルループには制約が多い。たとえば、上記のコードを説明する。

ここで、iはInduction Variableである。繰り返し文の条件では、< ,>, <=, >=, ==, !=のどれかを使わなければならず、しかもそのオペランドの片方は、識別子でなければならない。識別子はInduction Variableをささなければならない。for文の3つめのオペランドは、インクリメントやデクリメント、あるいはそれに準ずるような式(+=とかa = b + cとか)しか書けない。ループの本体でInduction Variableを書き変える場合は、インクリメントやデクリメントの類しか使えない。

ベクトルループには、チャンクサイズを指定できる。

for simd safelen(5) ( int i = 0 ; i < 10 ; ++i )
{
    X ;
    Y ;
}

ここで、式Xが式Yに先行しなければならない場合、i == 6の時の式Xの実行の前に、i == 5の時の式Yの実行は先行している保証がある。

とにかく制約が強いし、その制約を厳格に文面化するために、文面はかなりわかりにくい。

N4238: An Abstract Model of Vector Parallelism

並列実行版の<algorithm>で、純粋なベクトル実行ポリシーを設計するための、ベクトル実行の抽象的なモデルを提示する。この論文は提案ではない。

N4239: Defaulted Comparison Using Reflection

デフォルトの比較演算子をコンパイラーが生成しようという提案があるが、そういうものは現在提案されているリフレクション機能を使ってライブラリでやればよいという提案。

リフレクション機能が十分に柔軟であれば、比較演算子にとどまらず、+=とかhashの自動生成まで行える。また、比較方法について意見の一致が見られないので、なおさら多様な挙動を柔軟に切り替えられるライブラリでやるべきことだとの主張だ。

ドワンゴ広告

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

今日は、社内できのこの山派とたけのこの里派による宗教戦争が勃発したが、コーラタワー崩壊により終結したようだ。残念ながら、筆者は今回もコーラタワー崩壊を目にすることはかなわなかった。

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

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

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

No comments: