ブログ記事からの転載です。
初心者C++er Advent Calendar 2016 19日目の記事です。
クリスマスから少し過ぎてしまいましたが、最後に楽しくメタプログラミングしてみる記事でも。
初心者C++er Advent Calendar 2016 が全部うまるといいですね。
[メタプログラミングとは]
メタプログラミングの定義は言語によって異なりますが、C++ においては『テンプレート』を利用して『コンパイル時』に処理を行うことを指す事が一般的です。
今回はテンプレートを利用して『型に対して』コンパイル時に処理を行うコードを書いていきたいと思います。
[前提の知識]
[テンプレートの特殊化]
前提として『テンプレートの特殊化』を理解している必要があります。
『テンプレートの特殊化』というのはクラステンプレートなどに対して『特定の型』に対して処理を別に記述する記法です。
template<typename T>
struct X{
};
// 任意のテンプレート引数型に対して拡張する
// テンプレート引数が int の場合はこっちが使用される
template<>
struct X<int>{
};
メタプログラミングはこの特殊化を利用することが多いです。
[static_assert()
]
static_assert()
は『コンパイル時に意図的にエラーにするため』の機能です。
static_assert(定数式, 文字列リテラル);
のような形式で記述し、定数式
が false
の場合に 文字列リテラル
を出力してコンパイルエラーになります。
static_assert(3 > 1, "Error Message"); // OK コンパイルエラーにならない
static_assert(1 > 3, "Error Message"); // NG コンパイルエラーになる
メタプログラミングは static_assert()
を使用してテストする事が多いです。
参照:コンパイル時アサート - cpprefjp C++日本語リファレンス
[std::true_type
と std::false_type
]
今回つくるメタプログラミングは std::true_type
と std::false_type
を使用します。
この2つの型は単純に ::value
が true
と false
を返すだけのクラスになります。
std::true_type::value; // true
std::false_type::value; // false
この2つは標準ライブラリの <type_traits>
で
具体的な利用方法はこのあと書いていきます。
[メタ関数]
メタプログラミングはクラステンプレートに型を渡して処理を行ないます。
// hoge クラステンプレートに型を渡してその結果を ::type で取得する
hoge<int, float>::type
このように任意の型をクラステンプレートに渡して、その結果を ::type
という型で受け取ります。
また、このように型に対して処理するクラステンプレートのことを『メタ関数』を呼びます。
ちなみにメタ関数には型以外にも『値』を返すこともあります。
その場合は
foo<int, float>::value
というふうに ::value
で返すことが一般的です。
このようにメタ関数を使用、定義する場合はそのメタ関数が『型を返す』のか『値を返す』のかを意識する必要があります。
[型を比較する]
さて、まずは簡単に型を比較してみましょう。
動作イメージは以下のような感じです。
template<typename T, typename U>
struct is_same;
is_same<int, int>::value; // true
is_same<int, float>::value; // false
is_same<int, const int>::value; // false
このようにある型とある型が同じ型かどうかを比較するクラステンプレートを書いてみます。
#include <type_traits>
// 元となるクラステンプレート
// デフォルトでは std::false_type を継承する
template<typename T, typename U>
struct is_same : std::false_type{};
template<typename T>
// 2つのテンプレート引数が同じ型の場合の特殊化
// 同じ型の場合は std::true_type を継承する
struct is_same<T, T> : std::true_type{};
// ::value は std::true_type と std::false_type で定義されてる
static_assert(is_same<int, int>::value == true, "");
static_assert(is_same<int, float>::value == false, "");
static_assert(is_same<int, const int>::value == false, "");
こんな感じでテンプレートの特殊化を利用して実装します。
今回は比較した結果を『値』で返すので ::value
で結果を受け取ります。
ポイントとしては真になる場合は std::true_type
を継承し、偽になる場合は std::false_type
を継承してる点です。
これにより ::value
を定義することなく継承するだけで『真』や『偽』となるクラスが定義できます。
[型に const
などを付属させる]
任意の型に対して const
やポインタなどを付属させるメタ関数を定義してみます。
まずはポインタを追加するメタ関数です。
template<typename T>
struct add_const{
using type = T const;
};
static_assert(is_same<add_const<int>::type, int const>::value, "");
static_assert(is_same<add_const<int*>::type, int* const>::value, "");
static_assert(is_same<add_const<int const>::type, int const>::value, "");
template<typename T>
struct add_pointer{
using type = T*;
};
static_assert(is_same<add_pointer<int>::type, int*>::value, "");
static_assert(is_same<add_pointer<void>::type, void*>::value, "");
static_assert(is_same<add_pointer<float*>::type, float**>::value, "");
// 組み合わせて使用したり
static_assert(is_same<add_pointer<add_const<int>::type>::type, int const*>::value, "");
今回は結果が『値』ではなくて『型』なのでテンプレート型に対して const
や *
を付属した type
型を定義しています。
[const
やポインタを削除する]
では逆に const
やポインタを削除するメタ関数を定義してみましょう。
template<typename T>
struct remove_pointer{
using type = T;
};
template<typename T>
struct remove_pointer<T*>{
using type = T;
};
static_assert(is_same<remove_pointer<int*>::type, int>::value, "");
static_assert(is_same<remove_pointer<int**>::type, int*>::value, "");
static_assert(is_same<remove_pointer<int>::type, int>::value, "");
template<typename T>
struct remove_const{
using type = T;
};
template<typename T>
struct remove_const<T const>{
using type = T;
};
static_assert(is_same<remove_const<int const>::type, int>::value, "");
static_assert(is_same<remove_const<int* const>::type, int*>::value, "");
static_assert(is_same<remove_const<int>::type, int>::value, "");
先ほどのコードよりもちょっと複雑ですね。
const
やポインタの判定に特殊化を利用しています。
template<typename T>
struct X<T*>{};
というように記述することで『ポインタ型の場合のみ』という特殊化を行うことが出来ます。
[コンパイル時 FizzBuzz]
さて、最後にコンパイル時に FizzBuzz を行うメタ関数を書いてみましょう
[結果の型]
今回、FizzBuzz の結果は型として返します。
ですので最初にそれぞれの結果を型として定義しておきます。
struct Fizz{};
struct Buzz{};
struct FizzBuzz{};
// Fizz, Buzz, FizzBuzz 以外の場合はこの型をラップして返す
template<int N>
struct int_{};
数値の場合は『値』ではなくて『型』を返す必要があるので、それ用に『数値を受け取るクラステンプレート』も用意しておきます。
[if_else]
分岐処理を行うために if 文を模倣するメタ関数も用意しておきます。
template<bool Cond, typename Then, typename Else>
struct if_{
using type = Then;
};
template<typename Then, typename Else>
struct if_<false, Then, Else>{
using type = Else;
};
static_assert(is_same<if_<true, int, float>::type, int>::value, "");
static_assert(is_same<if_<false, int, float>::type, float>::value, "");
これも特殊化を利用してデフォルトは Then
を返し、Cond
が false
の場合は Else
を返すようにします。
[完成品]
先ほど書いたメタ関数を利用して FizzBuzz を実装すると以下のような感じになります。
struct Fizz{};
struct Buzz{};
struct FizzBuzz{};
template<int N>
struct int_{};
template<bool Cond, typename Then, typename Else>
struct if_{
using type = Then;
};
template<typename Then, typename Else>
struct if_<false, Then, Else>{
using type = Else;
};
static_assert(is_same<if_<true, int, float>::type, int>::value, "");
static_assert(is_same<if_<false, int, float>::type, float>::value, "");
template<int N>
struct fizzbuzz{
using type
= typename if_<N % 15 == 0, FizzBuzz,
typename if_<N % 3 == 0, Fizz,
typename if_<N % 5 == 0, Buzz,
int_<N> >::type >::type >::type;
// どっちが見やすいかな…
// using type = typename if_<
// N % 15 == 0,
// FizzBuzz,
// typename if_<
// N % 3 == 0,
// Fizz,
// typename if_<
// N % 5 == 0,
// Buzz,
// int_<N>
// >::type
// >::type
// >::type;
};
static_assert(is_same<fizzbuzz<1>::type, int_<1>>::value, "");
static_assert(is_same<fizzbuzz<2>::type, int_<2>>::value, "");
static_assert(is_same<fizzbuzz<3>::type, Fizz>::value, "");
static_assert(is_same<fizzbuzz<4>::type, int_<4>>::value, "");
static_assert(is_same<fizzbuzz<5>::type, Buzz>::value, "");
static_assert(is_same<fizzbuzz<6>::type, Fizz>::value, "");
static_assert(is_same<fizzbuzz<10>::type, Buzz>::value, "");
static_assert(is_same<fizzbuzz<15>::type, FizzBuzz>::value, "");
static_assert(is_same<fizzbuzz<30>::type, FizzBuzz>::value, "");
メタプログラミングではネストした if 文を記述するのが難読化してしまうのでちょっと見づらいですが、やってること自体は一般的な FizzBuzz と同じなのでそんなに複雑ではないです。
[まとめ]
と、言う感じで簡単にメタプログラミングについて書いてみました。
C++ でメタプログラミングというと敷居が高いイメージがありますが、細かいところ見ていくとそんなに複雑なことをしていないことがわかると思います。
メタプログラミングは C++ の醍醐味でもありますしね。
今回の記事でメタプログラミングに興味を持った方は標準ライブラリの <type_traits>
を調べてみたり Boost.MPL なんかを見てみるとよいと思います。
ちなみに今回は C++03 を少し意識して書いたんですが、C++11/14 では少しメタプログラミングが記述しやすくなっているので、それについても調べてみても面白いです。
それではよい C++ を〜。