web-dev-qa-db-ja.com

forループがコンパイル時の式ではないのはなぜですか?

タプルを反復するようなことをしたい場合は、クレイジーなテンプレートのメタプログラミングとテンプレートヘルパーの特殊化に頼らなければなりません。たとえば、次のプログラムは機能しません。

_#include <iostream>
#include <Tuple>
#include <utility>

constexpr auto multiple_return_values()
{
    return std::make_Tuple(3, 3.14, "pi");
}

template <typename T>
constexpr void foo(T t)
{
    for (auto i = 0u; i < std::Tuple_size<T>::value; ++i)
    {
        std::get<i>(t);
    }    
}

int main()
{
    constexpr auto ret = multiple_return_values();
    foo(ret);
}
_

iconstにすることはできません。そうしないと実装できません。しかしforループは、静的に評価できるコンパイル時の構造です。コンパイラは、as-ifルールのおかげで、それを自由に削除、変換、折りたたみ、展開、または好きなように実行できます。しかし、なぜconstexprの方法でループを使用できないのでしょうか。このコードには、「実行時」に実行する必要があるものはありません。コンパイラの最適化はその証拠です。

ループの本体内でiを変更できる可能性があることはわかっていますが、コンパイラはそれを検出できます。例:

_// ...snip...

template <typename T>
constexpr int foo(T t)
{
    /* Dead code */
    for (auto i = 0u; i < std::Tuple_size<T>::value; ++i)
    {
    }    
    return 42;
}

int main()
{
    constexpr auto ret = multiple_return_values();
    /* No error */
    std::array<int, foo(ret)> arr;
}
_

std::get<>()は、_std::cout.operator<<_とは異なり、コンパイル時の構成要素であるため、許可されない理由がわかりません。

20
user6416815

πάνταῥεῖは良い便利な答えを与えてくれましたが、_constexpr for_に関する別の問題について述べたいと思います。

C++では、最も基本的なレベルでは、すべての式に静的に(コンパイル時に)決定できる型があります。もちろん、RTTIや_boost::any_のようなものもありますが、これらはこのフレームワークの上に構築されており、式の静的型は、標準のいくつかのルールを理解するための重要な概念です。

次のような構文の空想を使用して、異種コンテナを反復できるとします。

_std::Tuple<int, float, std::string> my_Tuple;
for (const auto & x : my_Tuple) {
  f(x);
}
_

ここで、fはオーバーロードされた関数です。明らかに、これの意図された意味は、タプルのタイプごとにfの異なるオーバーロードを呼び出すことです。これが実際に意味することは、f(x)という式では、オーバーロードの解決を3回実行する必要があるということです。 C++の現在のルールでプレイする場合、これが意味を持つ唯一の方法は、基本的にループを3つの異なるループ本体に展開することですbefore式のタイプを理解しようとします。

コードが実際に

_for (const auto & x : my_Tuple) {
  auto y = f(x);
}
_

autoは魔法ではありません。「型情報がない」という意味ではなく、「型を推測してください、コンパイラーです」という意味です。しかし、明らかに、一般的には実際には3種類のyが必要です。

一方、この種の問題にはトリッキーな問題があります。C++では、パーサーは言語を正しく解析するために、タイプの名前とテンプレートの名前を知る必要があります。すべてのタイプが解決される前に、_constexpr for_ループのループ展開を行うようにパーサーを変更できますか?わかりませんが、簡単ではないかもしれません。多分もっと良い方法があります...

この問題を回避するため、現在のバージョンのC++ではビジターパターンを使用しています。オーバーロードされた関数または関数オブジェクトがあり、シーケンスの各要素に適用されるという考え方です。次に、各オーバーロードには独自の「本体」があるため、それらの変数の型や意味についてはあいまいさがありません。 _boost::fusion_や_boost::hana_のようなライブラリがあり、特定のビジターを使用して異種シーケンスを反復できます。forループの代わりにそれらのメカニズムを使用します。

Intだけで_constexpr for_を実行できる場合、たとえば、.

_for (constexpr i = 0; i < 10; ++i) { ... }
_

これは、異質なforループと同じ問題を引き起こします。 iを本体内のテンプレートパラメーターとして使用できる場合は、ループ本体のさまざまな実行でさまざまな型を参照する変数を作成できますが、式の静的型が何であるかが明確ではありません。

したがって、私にはわかりませんが、実際に_constexpr for_機能を言語に追加することに関連するいくつかの重要な技術的な問題があると思います。ビジターパターン/計画されたリフレクション機能は、頭痛の種のIMOにならないかもしれません...知っています。


私が考えたばかりの別の例を挙げましょう。これは、関連する困難を示しています。

通常のC++では、コンパイラーはスタック上のすべての変数の静的タイプを知っているため、その関数のスタックフレームのレイアウトを計算できます。

関数の実行中にローカル変数のアドレスが変化しないことを確認できます。例えば、

_std::array<int, 3> a{{1,2,3}};
for (int i = 0; i < 3; ++i) {
    auto x = a[i];
    int y = 15;
    std::cout << &y << std::endl;
}
_

このコードでは、yはforループの本体のローカル変数です。この関数全体で明確に定義されたアドレスがあり、コンパイラーによって出力されるアドレスは毎回同じです。

Constexprを使用した同様のコードの動作は何ですか?

_std::Tuple<int, long double, std::string> a{};
for (int i = 0; i < 3; ++i) {
    auto x = std::get<i>(a);
    int y = 15;
    std::cout << &y << std::endl;
}
_

重要なのは、xの型は、ループを通過するたびに異なる方法で推定されるということです。型が異なるため、スタック上でサイズと配置が異なる場合があります。 yはスタックの後にあるため、yはループの異なる実行でアドレスを変更する可能性があることを意味します-正しいですか?

yへのポインターがループの1つのパスで取得され、その後のパスで逆参照された場合の動作はどうなりますか?上記の_std::array_を使用した同様の "no-constexpr for"コードではおそらく合法であっても、未定義の動作である必要がありますか?

yのアドレスは変更できませんか?タプル内の最大の型がyの前に対応できるように、コンパイラーはyのアドレスを埋め込む必要がありますか?これは、コンパイラがループを展開してコードの生成を開始するだけではなく、事前にループのすべてのインスタンスを展開してから、各Nインスタンスからすべての型情報を収集する必要があることを意味しますか。満足のいくレイアウトを見つけますか?

パック拡張を使用する方が良いと思います。コンパイラーによって実装される方法、およびコンパイル時と実行時の効率性がより明確になります。

10
Chris Beck

http://stackoverflow.com/a/26902803/1495627 からヒントを得た、ボイラープレートをあまり必要としない方法を以下に示します。

template<std::size_t N>
struct num { static const constexpr auto value = N; };

template <class F, std::size_t... Is>
void for_(F func, std::index_sequence<Is...>)
{
  using expander = int[];
  (void)expander{0, ((void)func(num<Is>{}), 0)...};
}

template <std::size_t N, typename F>
void for_(F func)
{
  for_(func, std::make_index_sequence<N>());
}

その後、次のことができます。

for_<N>([&] (auto i) {      
  std::get<i.value>(t); // do stuff
});

C++ 17コンパイラにアクセスできる場合は、次のように簡略化できます。

template <class F, std::size_t... Is>
void for_(F func, std::index_sequence<Is...>)
{
  (func(num<Is>{}), ...);
}

Forループがコンパイル時の式ではないのはなぜですか?

for()ループは、c ++言語でランタイム制御フローを定義するために使用されるためです。

一般に、可変長テンプレートは、C++のランタイム制御フローステートメント内で解凍できません。

 std::get<i>(t);

iはランタイム変数であるため、コンパイル時に推定できません。

代わりに 可変テンプレートパラメータunpacking を使用してください。


また、この投稿が役立つ場合もあります(質問に対する回答のある重複を指摘していない場合)。

タプルの反復

4

C++ 20では、ほとんどのstd::algorithm関数はconstexprになります。たとえば、std::transform、ループを必要とする多くの操作はコンパイル時に実行できます。この例を考えてみてくださいコンパイル時に配列内のすべての数値の階乗を計算しますBoost.Hanaのドキュメント から変更):

#include <array>
#include <algorithm>

constexpr int factorial(int n) {
    return n == 0 ? 1 : n * factorial(n - 1);
}

template <typename T, std::size_t N, typename F>
constexpr std::array<std::result_of_t<F(T)>, N>
transform_array(std::array<T, N> array, F f) {
    auto array_f = std::array<std::result_of_t<F(T)>, N>{};
    // This is a constexpr "loop":
    std::transform(array.begin(), array.end(), array_f.begin(), [&f](auto el){return f(el);});
    return array_f;
}

int main() {
    constexpr std::array<int, 4> ints{{1, 2, 3, 4}};
    // This can be done at compile time!
    constexpr std::array<int, 4> facts = transform_array(ints, factorial);
    static_assert(facts == std::array<int, 4>{{1, 2, 6, 24}}, "");
}

"loop"を使用して、コンパイル時に配列factsを計算する方法、つまりstd::algorithm。これを書いている時点で、最新のclangまたはgccリリースの実験バージョンが必要です godbolt.org で試してみてください。しかし、間もなくC++ 20は、リリースバージョンのすべての主要なコンパイラによって完全に実装されます。

0
Romeo Valentin