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

【初心者C++er Advent Calendar 2016 19日目】C++ 楽しいメタプログラミングしよう

初心者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_typestd::false_type]

今回つくるメタプログラミングstd::true_typestd::false_type を使用します。
この2つの型は単純に ::valuetruefalse を返すだけのクラスになります。

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, "");

http://melpon.org/wandbox/permlink/VNE2apvsZ2Hq21Le

こんな感じでテンプレートの特殊化を利用して実装します。
今回は比較した結果を『値』で返すので ::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, "");

http://melpon.org/wandbox/permlink/GtOPyLNLgxyJK2jX

今回は結果が『値』ではなくて『型』なのでテンプレート型に対して 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, "");

http://melpon.org/wandbox/permlink/dmYesP6x8QV9GZOK

先ほどのコードよりもちょっと複雑ですね。
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 を返し、Condfalse の場合は 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, "");

http://melpon.org/wandbox/permlink/pEnLxRi5MXxKJ83C

メタプログラミングではネストした if 文を記述するのが難読化してしまうのでちょっと見づらいですが、やってること自体は一般的な FizzBuzz と同じなのでそんなに複雑ではないです。

[まとめ]

と、言う感じで簡単にメタプログラミングについて書いてみました。
C++メタプログラミングというと敷居が高いイメージがありますが、細かいところ見ていくとそんなに複雑なことをしていないことがわかると思います。
メタプログラミングC++ の醍醐味でもありますしね。
今回の記事でメタプログラミングに興味を持った方は標準ライブラリの <type_traits> を調べてみたり Boost.MPL なんかを見てみるとよいと思います。
ちなみに今回は C++03 を少し意識して書いたんですが、C++11/14 では少しメタプログラミングが記述しやすくなっているので、それについても調べてみても面白いです。
それではよい C++ を〜。