C++ コア ガイドライン:テンプレート メタプログラミングのルール

はい、あなたはそれを正しく読みました。今日は、値ではなく型でプログラミングするテンプレート メタプログラミングについて書きます。

ガイドラインのテンプレート メタプログラミングの紹介は、「必要な構文とテクニックはかなり恐ろしいものです。」というユニークな終わり方をしています。したがって、ルールは主に禁止事項に関するものであり、内容はあまりありません:

  • T.120:テンプレート メタプログラミングは、本当に必要な場合にのみ使用してください
  • T.121:テンプレート メタプログラミングは主に概念をエミュレートするために使用します
  • T.122:テンプレート (通常はテンプレート エイリアス) を使用してコンパイル時に型を計算する
  • T.123:constexpr を使用 コンパイル時に値を計算する関数
  • T.124:標準ライブラリの TMP 機能を使用することを好む
  • T.125:標準ライブラリの TMP 機能を超える必要がある場合は、既存のライブラリを使用してください

正直なところ、テンプレートのメタプログラミングがそれほど恐ろしいものだとは思いませんが、構文にはまだ多くの可能性があります。

テンプレートのメタプログラミングをわかりやすく説明し、コンパイル時のプログラミング全般について書きます。このコンパイル時のプログラミングの紹介では、型特性 (T.124:標準ライブラリの TMP 機能を使用することをお勧めします) と constexpr 関数 (T.123:constexpr を使用する) について明示的に記述します。 コンパイル時に値を計算する関数)、暗黙的に他のルールを参照します。これが私の計画です:

テンプレート メタプログラミングを紹介し、型特性ライブラリを使用して適切に構造化され移植可能な方法でテンプレート メタプログラミングを使用する方法と、constexpr 関数を使用してテンプレート メタプログラミング マジックを通常の関数に置き換える方法を示します。

テンプレート メタプログラミング

すべての始まり

1994 年、Erwin Unruh は C++ 委員会で、コンパイルされないプログラムを発表しました。これはおそらく、コンパイルされたことのない最も有名なプログラムです。

// Prime number computation by Erwin Unruh
template <int i> struct D { D(void*); operator int(); };

template <int p, int i> struct is_prime {
 enum { prim = (p%i) && is_prime<(i > 2 ? p : 0), i -1> :: prim };
 };

template < int i > struct Prime_print {
 Prime_print<i-1> a;
 enum { prim = is_prime<i, i-1>::prim };
 void f() { D<i> d = prim; }
 };

struct is_prime<0,0> { enum {prim=1}; };
struct is_prime<0,1> { enum {prim=1}; };
struct Prime_print<2> { enum {prim = 1}; void f() { D<2> d = prim; } };
#ifndef LAST
#define LAST 10
#endif
main () {
 Prime_print<LAST> a;
 } 

Erwin Unruh は Metaware Compilers を使用しましたが、このプログラムは C++ では有効ではなくなりました。著者による新しいバリアントはこちらです。では、なぜこの番組が有名になったのでしょうか。エラーメッセージを見てみましょう。

重要な部分を赤でハイライトしました。パターンがわかると思います。プログラムは、コンパイル時に最初の 30 個の素数を計算します。これは、テンプレートのインスタンス化を使用してコンパイル時に計算できることを意味します。それはさらに良いです。テンプレート メタプログラミングはチューリング完全であるため、あらゆる計算問題の解決に使用できます。 (もちろん、再帰の深さ (C++11 では少なくとも 1024) と、テンプレートのインスタンス化中に生成される名前の長さにより、いくつかの制限が生じるため、チューリング完全性はテンプレート メタプログラミングの理論上のみ成立します。)

魔法の仕組み

従来の方法から始めましょう。

コンパイル時の計算

数値の階乗を計算することは、テンプレート メタプログラミングの「Hello World」です。

// factorial.cpp

#include <iostream>

template <int N> // (2)
struct Factorial{
 static int const value = N * Factorial<N-1>::value;
};

template <> // (3)
struct Factorial<1>{
 static int const value = 1;
};

int main(){
 
 std::cout << std::endl;
 
 std::cout << "Factorial<5>::value: " << Factorial<5>::value << std::endl; // (1)
 std::cout << "Factorial<10>::value: " << Factorial<10>::value << std::endl;
 
 std::cout << std::endl;

}

行 (1) の factorial<5>::value の呼び出しにより、行 (2) でプライマリ テンプレートまたは一般テンプレートがインスタンス化されます。このインスタンス化中に、Factial<4>::value がインスタンス化されます。この再帰は、完全に特殊化されたクラス テンプレート Factorial<1> が行 (3) で開始されると終了します。たぶん、あなたはそれがより絵のようなものを好むでしょう.

プログラムの出力は次のとおりです。

くそー、値がコンパイル時に計算されたことを証明するのをほとんど忘れていました。これがコンパイラ エクスプローラです。簡単にするために、メイン プログラムと対応するアセンブラー命令のスクリーンショットのみを提供します。

最初の黄色い線と最初の紫の線がそれを示しています。 5 と 10 の階乗は単なる定数であり、コンパイル時に計算されました。

正直なところ、factorial プログラムは素晴らしいプログラムですが、テンプレート メタプログラミングには慣用的ではありません。

コンパイル時の型の操作

コンパイル時の型の操作は、通常、テンプレートのメタプログラミング用です。信じられないなら、std::move を勉強してください。 std::move が概念的に行っていることは次のとおりです。

static_cast<std::remove_reference<decltype(arg)>::type&&>(arg);

わかった。 std::move は引数 arg を取り、そこから型 (decltype(arg)) を推測し、参照を削除 (remove_reverence) し、それを右辺値参照 (static_cast<...>::type&&>) にキャストします。本質的に、これは std::move が常に右辺値参照型を返すことを意味し、したがってムーブ セマンティックはそれをキックできます。

型特性ライブラリからの std::remove_reference はどのように機能しますか?これは、引数から constness を削除するコード スニペットです。

template<typename T > 
struct removeConst{ 
 typedef T type; // (1)
};

template<typename T > 
struct removeConst<const T> { 
 typedef T type; // (1)
};


int main(){
 
 std::is_same<int, removeConst<int>::type>::value; // true
 std::is_same<int, removeConst<const int>::type>::value; // true
 
}

std::remove_const がおそらく型特性ライブラリに実装されている方法で removeConst を実装しました。型特性ライブラリの std::is_same は、コンパイル時に両方の型が同じかどうかを判断するのに役立ちます。 removeConst の場合、最初のまたは一般的なクラス テンプレートが作動します。 removeConst の場合、const T の部分的な特殊化が適用されます。重要な観察は、両方のクラス テンプレートが行 (1) で基になる型を返すため、constness が削除されていることです。

次は?

次回の投稿では、コンパイル時のプログラミングの紹介を続けます。これは特に、型特性ライブラリに入る前に、関数とメタ関数を比較することを意味します.