読者です 読者をやめる 読者になる 読者になる

ボレロ村上 - ENiyGmaA Code

中3女子です。

次期C++ではコンパイル時レイトレーシングやパーサコンビネ―タや音声合成が標準ライブラリで提供される

※これはエイプリルフール記事です


C++1yでは、レイトレーシングやパーサコンビネ―タや音声合成が、標準ライブラリに含まれることが決定している。
もちろんこれらはconstexprで実装され、コンパイル時に実行することができる。


しかしながら、C++11のconstexprではあまりに制限が多いため実装が困難である。
現在C++標準化委員会では、constexprの制限を大幅に緩和すべく提案がなされている。


言うまでもなくこれらの提案は、コンパイルレイトレーシングのパーサコンビネ―タや音声合成を実装するためのものである。
その次のC++2z(仮)では、更にコンパイルフォトンマッピングコンパイル時ウェーブレット変換、コンパイル時動画編集などが追加される予定だという。


さて今回は、constexprの制限緩和を提案するN3597を翻訳してみた。
中3女子レベルの英語力によるものなので、著しい誤訳があれば指摘いただきたい。


原文:Relaxing constraints on constexpr functions

constexpr関数の制限緩和

概要

プログラミング言語の機能は、それらが直交しているほど便利で理解しやすく、また互いに自然に組み合わせることができる。
constexpr関数が現在抱えている、他の多くの言語機能との自然な組合せを妨げる数々の制限(例えば、forループ、変数の変更、例外など)のため、記述するのが難しくなっている。
これらの制限を回避する作業は、多くの場合、表現力を犠牲にする必要があり、プログラマのフラストレーションを引き起こす。


本稿では、それらを実行時C++コードのようによりシンプルで統一的にするために、constexpr関数定義の制限のほとんどを除去することを検討する。
慣用的なC++コードは、constexprキーワードの追加以外、非constexprな書き方から殆どまたはまったく変更することなく、constexpr関数内で許可されるであろう。

問題

以前の N3268 では、constexpr関数の本体は、次の形式にする必要があった

{ return expression; }


N3268 は、(7.1.5/3)を許可するようにルールを緩めた:

    • 空文
    • static_assert宣言
    • typedef宣言と、クラスまたは列挙型を定義しないエイリアス宣言
    • using宣言
    • usingディレクティブ
    • および、たった一つのreturn文


これらのconstexpr関数定義の制限事項は依然として非常に厳しく、またルールの緩和は結果的に教育を難しくし、それを正当化することにもなった。
非自明なconstexpr関数が複雑になると、もし既に純粋関数型的なインタフェースであったとしても、コードを構文上の制約の範囲内に収まるようにねじ曲げなければならないため、多くの人にとって馴染みのないコーディングスタイルしならざるをえない。


std::bitset::allを考えてみる。可能な実装の一つは次のとおり:

template <size_t N>
bool bitset<N>::all() const noexcept {
  if (std::any_of(storage, storage + num_full_words, [] (word w) { return ~w; }))
    return false;
  if (num_full_words != num_words && storage[num_full_words] != last_word_mask)
    return false;
  return true;
}

このコードはシンプルかつ慣用的であり、他のライブラリコンポーネントを使用することもできる。しかし、この関数のconstexpr版を作りたい場合、我々はそれを書き換える必要がある。

constexpr bool any_unset(word *it, word *end) {
  return it == end ? false :
         ~*it ? true :
         any_unset(it + 1, end);
}

template <size_t N>
constexpr bool bitset<N>::all() const noexcept {
  return !any_unset(storage, storage + num_full_words) &&
         (num_full_words == num_words ||
          storage[num_full_words] == last_word_mask);
}

この実装は、constexprの制限のいくつかに苦しんでいる:

    • イテレータはconstexpr関数でインクリメントすることができないため、標準ライブラリのアルゴリズムの殆どはconstexprではない。我々はstd::any_ofがconstexpr互換であるよう、再実装しなければならない。
    • ラムダはconstexpr関数で禁止されているので、我々の再実装にラムダを渡すことはできない。
    • ループが禁止されているので、配列を走査するため再帰を使用する必要がある。
    • ifが禁止されているので、代わりに ?:, &&, および || を使用する必要がある。

代替


ポートランド(2012年10月)での議論では、単純なforループのサポートはconstexprのルールを十分緩和するための最小要件であることを確認している。この要件は、いくつかの方法によって達成することができる:

    • constexprが必要とする関数型プログラミングスタイルとうまく相互作用するような、新しいループ構造の言語への追加。これはループ構造の欠如を直接解決するが、既存の言語構造に対するプログラマのフラストレーションを無くすことにはならない。
    • 伝統的なC言語の構文の、最小限の機能セットの許可。ループの反復ごとに異なる動作をさせるためには、constexpr関数の評価の際に、ローカル変数の変更について最小限のサポートを必要とする。これは組込整数型Tについて、 for (T var = expr1; var != expr2; ++var) という形式に制限される可能性がある。
    • range-based for をサポートする最小限の機能セットの許可。ループの複数反復が相互通信できるように、追加的な機構が必要となるだろう。このようなループをユーザ定義のイテレータ型と共に使用するのは、更なる言語の制限緩和なしにはできなかった。
    • constexpr関数で使用される可能性があるC++全般についての、一貫した広範なサブセットの許可。


最初の選択肢は、constexpr部分と「それ以外のC++」部分において、C++言語を更に破壊するリスクがある。
2番目と3番目の選択肢は、定数式の評価にフロー制御と変数の変更を加えることの両方を必要とし、constexpr関数において今日より多くの恣意的な制限が見られるようになるだろう。
そこで我々は、最後の選択肢を詳細に検討し、forループのサポートに必要な範囲を超えて、実装のシンプルさを犠牲にすることなく使いやすさを改善するための、C++の適切なサブセットを模索することにした。


我々は、すべての主要な実装者が定数式中でのサポートを期待することが理的であるようなC++のサブセットとなるよう、細心の注意を払う必要がある。
さらに、翻訳時と実行時の区別を維持することは重要であり、翻訳環境でサポートできないコンストラクトの許可は避けねばならない(例えば、翻訳時のnewと実行時のdeleteに対応するには、重大な実装上の問題があるだろう)。

提案されたソリューション


constexprによる、ほぼ制限のないコンパイル時関数評価機構を推進する。
プログラミング言語Dではそのような機構の実装実績があり、それはポピュラーな機能である(この機能についてはドキュメントを参照)。
プログラマのモデルはシンプルになり、constexprによって彼らのコードはコンパイル時に実行することができるようになる。

定数式

C++の抽象マシンの規則に従って、次のものの評価が含まれていなければ、式は定数式である。

    • 静的記憶域期間のオブジェクト(もしあれば、構築中のオブジェクト以外)の変更のような、グロバールから可視の副作用
    • 動的メモリ割り当てや、不定な結果との比較、評価中に定数として作成されていないオブジェクトの左辺値から右辺値への変換など、翻訳時に評価できない表現。
    • 型安全性を侵害したり、(例えばreinterpret_castによって)抽象マシンの基本記憶域を検査すること、またはインラインasmの使用など、可搬性のないコンストラクト
    • 非constexpr関数の呼び出し、または
    • 未定義動作。

その結果の値が完全に初期化され、テンポラリを指す任意の参照やポインタを含まず、または自動、動的、あるいはスレッド記憶域期間を持つオブジェクト。


C++11における関数呼び出しの置換(function invocation substitution)は、このモデルでは必要ない。constexpr関数の呼び出しではなくC++の抽象マシンによって通常どおり処理される。


実装のシンプルさに対する懸念のために、ラムダ式、スロー式、および非自明なデストラクタを持つオブジェクト作成の評価は、非定数式となる。

定数式内のオブジェクトの変更

定数式内で作成されたオブジェクトは、その定数式の評価完了またはオブジェクトの存続期間終了のうち早い方まで、定数式が評価される範囲内において変更することができる(任意のconstexpr関数呼出はこの評価に含まれる)。
それらを後から定数式の評価によって変更することはできない。
例:

constexpr int f(int a) {
  int n = a;
  ++n;                  // '++n' is not a constant expression
  return n * a;
}

int k = f(4);           // OK, this is a constant expression.
                        // 'n' in 'f' can be modified because its lifetime
                        // began during the evaluation of the expression.

constexpr int k2 = ++k; // error, not a constant expression, cannot modify
                        // 'k' because its lifetime did not begin within
                        // this expression.

struct X {
  constexpr X() : n(5) {
    n *= 2;             // not a constant expression
  }
  int n;
};
constexpr int g() {
  X x;                  // initialization of 'x' is a constant expression
  return x.n;
}
constexpr int k3 = g(); // OK, this is a constant expression.
                        // 'x.n' can be modified because the lifetime of
                        // 'x' began during the evaluation of 'g()'.

このアプローチは、評価中に任意の変数の変更を可能にしながらも、定数式の評価はプログラムの変更可能なグローバルの状態とは無関係であるという本質的な特性を維持する。
したがって定数式は、値が不定である場合を除いて、問題なく同じ値に評価される。(例えば浮動小数点演算が異なる結果を与えた場合、これらの変更によって、異なる評価順序で異なる結果を与えることもできる)。


評価中に寿命が開始しないオブジェクトの使用に関する規則は変更なく、いずれかの場合読み取り(しかし変更はできない)することができる。

    • constexprによって宣言されている、または
    • 整数定数もしくはスコープを持たない列挙型。
constexpr関数

C++11のように、constexprキーワードは、それが定数式が要求されるコンテキストから使用される場合に、処理系が翻訳時に評価する必要がある機能をマークするために使用される。
任意の有効なC++コードはconstexpr関数で許可され、ローカル変数の作成と変更を含めたほとんどすべての文は、constexpr関数のために定数式中で可能でなければならない。
定数式は、評価とその結果に対してローカルな副作用を持つかもしれない。
例えば:

constexpr int min(std::initializer_list<int> xs) {
  int min = std::numeric_limits<int>::max();
  for (int x : xs)
    if (x < min)
      min = x;
  return min;
}

constexpr int fn(int a) {
  return a / (a - a); // ill-formed, no diagnostic required, never constant
}

constexpr関数の構文上の制限のうち一握りは保持される。

    • asm 宣言は許可されない。
    • tryブロックおよび関数tryブロックは許可されない。
    • 静的およびスレッド記憶域期間を持つ変数の宣言は、いくつかの制限事項(下記参照)がある。
constexprコンストラクタ

任意のconstexprコンストラクタでは、構築中オブジェクトの寿命は周囲の定数式(もしあれば)の評価中に開始されたものであるため、コンストラクタの評価およびそれ以降の部分で、フィールドを変更することが許可されている。
例:

struct lookup_table {
  int value[32];
  constexpr lookup_table() {
    for (int n = 0; n < 32; ++n) {
      double x = n / 4;
      double f = x * std::cbrt(x) * std::pow(2, (n & 3) * 0.25);
      value[n] = (int)(f * 1000000.);
    }
  }
  // OK, would be an error if implicit ~lookup_table was not constexpr.
  constexpr ~lookup_table() = default;
};
constexpr lookup_table table; // OK, table has constant initialization, and
                              // destruction is a constant expression.

struct override_raii {
  constexpr override_raii(int &a, int v) : a(a), old(a) {
    a = v;
  }
  constexpr ~override_raii() {
    a = old;
  }
  int &a, old;
};

constexpr int h(const lookup_table &lut) { /* ... */ }
constexpr int f() {
  lookup_table lut;
  override_raii override(lut.value[4], 123);
  return h(lut);
  // OK, destructor runs here.
}
ブロックスコープの静的ローカル変数

constexpr関数に静的またはスレッド記憶期間の変数の宣言が含まれている場合、副作用が評価されることを防ぐため、いくつかの追加の制限が必要となる。

    • そのような変数は定数式で初期化する必要がある。これによって、変数の初期化値が、constexpr関数呼び出しの評価順序に依存するような実装を防ぐ。
constexpr int first_val(int n) {
  static int value = n; // error: not a constant expression
  return value;
}
const int N = first_val(5);
int arr[first_val(10)];
    • このような変数のデストラクタは自明でなければならない。 これによって、実装がプログラム終了時に副作用を引き起こすかどうかを気にすることなく、constexpr関数呼出しを評価することができる。
    • このような変数は、その生存期間が定数式の評価中に開始した場合でも、変更することはできない。


他のすべての点で、そのような静的またはスレッドローカル変数は、それが関数の外側で宣言された場合と同じように、constexpr関数内で使用することができる。
特に、その値が使用されないならば、constexprでもリテラル型である必要もない。

constexpr mutex &get_mutex(bool which) {
  static mutex m1, m2; // non-const, non-literal, ok
  if (which)
    return m1;
  else
    return m2;
}

可能な追加機能

定数式中の言語機能について、実装コストを十分正当化できると考えられるならば、constexpr関数と定数式の評価における残りの制限の一部を緩和することができるだろう。

constexprデストラクタ

ほとんどの場合、定数式でT型のオブジェクトを作成するためには、Tのデストラクトは自明である必要がある。
ただし、非自明なデストラクタは、モダンなC++の重要な構成要素として広範に使用されるRAIIイディオムを、constexprの評価にも部分的に適用可能とするのに必要である。
非自明なデストラクタは、次のように定数式でサポートすることができる:

    • デストラクタのconstexprとしてのマークを許可する
    • constexprデストラクタの呼出だけならば、constexprなデフォルトデストラクタが作成される
    • constexprの変数の場合は、デストラクタの評価が定数式であることを必要とする(ただし、破棄されるオブジェクトは、独自のデストラクタでの変更が可能である

しかしながら、この機能の魅力的なユースケースは不明であるし、デストラクタが適切なタイミングで実行されるには確実に非自明な実装コストがあるだろう。

ラムダ

N2859 のノートによれば、ラムダがそのコンテキストでマングルされた名前の一部を要求される場合、深刻な実装の困難が生じるだろうし、定数式でのラムダの禁止の解決策が困難であることの一因である。
また、定数式でラムダを許可することについて、実装コストに関する懸念が提起されているため、ここでは提案しない。

例外

定数式の評価中で例外のスローとキャッチをサポートすることは可能だろうが、我々はその魅力的なユースケースが不明なため、ここでは提案しない。

可変引数関数

constexpr関数内でCスタイル可変引数関数とva_argマクロをサポートすることは可能だろうが、可変引数関数テンプレートが存在する以上無価値と考えられるため、ここでは提案しない。

謝辞

筆者は、この提案に励ましや考察を寄せてくれたBjarne StroustrupとGabriel Dos Reisへの感謝の意を表し、また論文の草稿に対しコメントと訂正を頂いた Lawrence Crowl、Jeffrey Yasskin、Dean Michael BerrisおよびGeoffrey Romerに感謝します。