【初心者C++er Advent Calendar 2016 21日目】C++ でプリプロセスマクロを使う場面
初心者C++er Advent Calendar 2016 21日目の記事です。
さて、巷ではマクロを使用したコードを公開すると『static const
を使え』や『typedef
しろ』、『関数テンプレートでおk』などと斧が飛んでくる突っ込まれることが多いです。
これは C++ では『基本的にはマクロを使うべきではなく』さらに『マクロを使わないでよいコード』に直せることが多いからです。
では逆に『マクロを使うべきコード』というのはどういうものなのでしょうか。
この記事では『マクロを使うべきコード』の実例をいくつか紹介してみたいと思います。
[インクルードガード]
ヘッダーファイルを記述する際に『重複インクルードを防止するため』にインクルードガードという技法が使用されれます。
これは #ifndef
を利用して『一度だけヘッダーファイルが読み込まれる』ような仕組みです。
// hoge.h #ifndef HOGE_H #define HOGE_H // ここに実装を記述する #endif // HOGE_H
これにより複数回 #include "hoge.h"
しても一度だけ定義が読み込まれます。
ただ、最近は同等の機能である #pragma once
を実装してるコンパイラもあるので、そちらを使用している人もいます。
[キーワードなどを置き換える]
さて、次のように constexpr
を使ったコードがあるとします。
constexpr auto plus(int a, int b){ return a + b; } constexpr auto minus(int a, int b){ return a - b; } static_assert(plus(1, 2) == 3, ""); static_assert(minus(4, 5) == -1, "");
これは C++11 に対応してるコンパイラであれば問題なく動作します。
しかし、環境によってはどうしても C++03 に対応しなければならないこともあります。
こういう場合、constexpr
というキーワードをマクロに置き換えることで、複数の環境に柔軟に対応する事が出来ます。
// C++11 以上ならば #if __cplusplus >= 201103L // constexpr をマクロに置き換える #define CONSTEXPR constexpr #else // C++11 以上に対応してないなら空のマクロを定義 #define CONSTEXPR #endif // constexpr をマクロに置き換える CONSTEXPR int plus(int a, int b){ return a + b; } CONSTEXPR int minus(int a, int b){ return a - b; } #if __cplusplus >= 201103L static_assert(plus(1, 2) == 3, ""); static_assert(minus(4, 5) == -1, ""); #endif
このように『マクロ以外』で置き換えることが出来ないようなワークアラウンドに対応する場合に利用できます。
[コンパイルするときに挙動を変更したい場合]
例えば、次のように『デバッグコードを仕込んだコード』があるとします。
int plus(int a, int b){ // DEBUG が定義されている時のみログを出力したい #ifndef DEBUG std::cout << a << std::endl; std::cout << b << std::endl; #endif return a + b; }
このように『マクロを使用すること』でコンパイル時に『挙動を変更する』事が出来ます
# DEBUG マクロを定義してコンパイルする $ g++ main.cpp -DDEBUG $ ./a 1 2 3
gcc だとこんな感じでコンパイル時にマクロを定義する事が出来ます。
[まとめ]
と、言う感じでパッと思いついたものを上げてみました。
他にも使うべき場面はありますが、初心者向けならこのあたりでしょうか。
C++ では『マクロを使うべきではないコード』もたくさんありますが『マクロでしか実装出来ないコード』もまた存在します。
マクロは用法・用量を守って正しくお使いください。
Ruby 2.4 で refinements の制限が緩和された
さて、先日 Ruby 2.4 がリリースされました。
[refinements の制限を緩和]
Ruby 2.4 では refinements の制限が緩和され、#send
からも refine
されているメソッドが呼び出せるようになりました。
class X; end module Ex refine X do def homu p "homu" end end end using Ex x = X.new x.send :homu # => "homu"
日頃から refinements クソだ!と言ってる勢としては『やっと対応されたか…』という感じ。
今後も使い勝手がよくなるようにここら辺の制限の調整がすすむといいですねえ…。
あとリリース前に Symbol#to_proc
内からも呼び出させるようになると聞いたんですが、こっちはまだエラーでした。
# Error: undefined method `homu' for #<X:0x005564eebeba60> (NoMethodError) :homu.to_proc.call x
【初心者C++er Advent Calendar 2016 20日目】関数テンプレートの呼び出しに優先順位をつける
初心者C++er Advent Calendar 2016 20日目の記事です。
クリスマスが終わって『もはや Advent Calendar とは…』となってるんですが、まあ埋まってないより埋まってるほうがいいかなーと。
今回は初心者向けかどうか怪しい内容ですが、ちょっとしたハックでも。
なお、この記事は SFINAE を知ってる事が前提の記事になります。
[コード]
さて、次のように SFINAE を利用して『a + a
ができる型(値)のみ』受け付けたい関数テンプレートを定義したいことがあると思います。
template<typename T> auto twice(T a) // decltype() 内で式を記述することで a + a を要求する ->decltype(a + a){ return a + a; } // 42 + 42 は有効なので OK twice(42); // 84 // 3.14f + 3.14f も有効なので OK twice(3.14f); // 6.28 // "homu" + "homu" は無効なので NG twice("homu");
まあここまではよくあるコードだと思います。
[問題]
このような関数テンプレートを定義した場合に『それ以外にマッチする関数』も定義したいことがあると思います。
// a + a が有効でない引数の場合に呼び出す関数を定義 template<typename T> auto twice(T t){ return t; } twice("homu"); // "homu"
これでよかったよかった…とはならずに、次のように『a + a
ができる型(値)』を渡した場合にエラーになります。
// Error: call of overloaded 'twice(int)' is ambiguous twice(42);
http://melpon.org/wandbox/permlink/GqFem4RxFct7gvqF
これは twice(int)
時に『両方の関数に該当するため』呼び出しが解決できないためです。
[解決]
解決策はいくつかあるんですが、次のようにして『オーバーロードを解決するため引数』を追加することで比較的簡単に回避することが出来ます。
// 引数に int を追加する template<typename T> auto twice(int, T a) ->decltype(a + a){ return a + a; } // 引数に bool を追加する template<typename T> auto twice(bool, T t){ return t; } // int 型の値を関数に渡すことで最初に定義した関数の方を優先して呼び出されるようになる twice(0, 42); // 84 twice(0, 3.14f); // 6.28 twice(0, "homu"); // "homu"
http://melpon.org/wandbox/permlink/WeEUFMV8MGEZRkyb
これは twice(0, 42)
の場合は twice(bool, T)
よりも twice(int, T)
にマッチし、twice(0, "homu")
の場合は SFINAE によって twice(int, T)
がオーバーロードの候補から外れ、0
が bool
型に暗黙の型変換されるので twice(bool, T)
にマッチするためです。
まあ、要するに
void func(int); void func(bool); // func(int) を呼び出す func(0);
ということを利用しているだけですね。
[ちょっと改良]
上記のままだと毎回 0
を渡す必要があるのでラッパ関数を用意すると使い勝手がよくなります。
template<typename T> auto twice_impl(int, T a) ->decltype(a + a){ return a + a; } template<typename T> auto twice_impl(bool, T t){ return t; } template<typename T> auto twice(T t){ // ラッパ関数を用意して、その中で twice の実装を呼び出す return twice_impl(0, t); } twice(42); // 84 twice(3.14f); // 6.28 twice("homu"); // "homu"
http://melpon.org/wandbox/permlink/lwTkvXx39ptEP6TN
これでユーザコードからこのハックを隠蔽する事が出来ました。
[まとめ]
と、言う感じで簡単なハックを紹介してみました。
SFINAE を使った関数を定義しているとこういう『オーバーロードが解決できない関数』が結構出てくるんで地味に重宝します。
もうちょっと汎用的にしたコードが下記の記事に載ってるので気になる方は読んでみるとよいです。
[参照]
【初心者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_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, "");
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
を返し、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, "");
http://melpon.org/wandbox/permlink/pEnLxRi5MXxKJ83C
メタプログラミングではネストした if 文を記述するのが難読化してしまうのでちょっと見づらいですが、やってること自体は一般的な FizzBuzz と同じなのでそんなに複雑ではないです。
[まとめ]
と、言う感じで簡単にメタプログラミングについて書いてみました。
C++ でメタプログラミングというと敷居が高いイメージがありますが、細かいところ見ていくとそんなに複雑なことをしていないことがわかると思います。
メタプログラミングは C++ の醍醐味でもありますしね。
今回の記事でメタプログラミングに興味を持った方は標準ライブラリの <type_traits>
を調べてみたり Boost.MPL なんかを見てみるとよいと思います。
ちなみに今回は C++03 を少し意識して書いたんですが、C++11/14 では少しメタプログラミングが記述しやすくなっているので、それについても調べてみても面白いです。
それではよい C++ を〜。
【初心者C++er Advent Calendar 2016 17日目】C++ で型と値を理解する
初心者C++er Advent Calendar 2016 17日目の記事です。
初心者ネタに型と値の話でも。
[値とは]
C++ における値とは数値や文字列、クラスのインスタンスなどを指します。
// 数値や小数 42 3.14f // 文字や文字列 'c' "homu" // クラスのインスタンス std::string{} std::vector<int>{}
まあ要はデータそのものですね。
[型とは]
一方、C++ における型とは int
や char
、std::string
などを指します。
型は一般的に『何かの値を受け取るものを定義する時』つまり『変数を定義する時』や『関数の引数を定義する時』などに使用します。
// 42 という『値』を受け取る変数を『int 型』として定義する int n = 42; // 『int 型』と『float 型』を受け取って『bool 型』を返す関数を宣言する bool func(int, float); // 関数に『値』を渡して『値』を受け取る bool b = func(42, 3.14f);
このように型とは『値と対になるような存在』になりどんな値にも『型』は存在します。
[型による制約]
基本的には『同じ型』の変数に対してのみ値を代入することが出来ます。
int a; // int 型の変数を定義 a = -10; // int 型の値を代入する a = "homu"; // Error: 他の型を代入することは出来ない
このようにすることで任意の変数は『定義した時の型』である事が保証されます。
ただし、値によっては『別の型』を代入する事ができる場合もあります。
float b; // float 型の変数を定義 b = 3.14f; // float 型の値を代入する b = 42; // OK: int 型の値を代入してもエラーにならない
このように他の型へ変換されることを暗黙の型変換と呼びます。
C++ ではしばしばこのように暗黙的に別の型へと変換される事があるので注意してください。
このように C++ では『値』がどんな『型』になるのか、また『値を演算した結果(関数を呼び出した結果)』どんな『型に変換されるのか』を意識して書くことが重要になってきます。
これに慣れてくると
result = 42 + 3.14f;
というような処理が
float = int + float;
のように型に脳内変換されます。
[値から型を定義する]
さて、ちょっと型の話をしていきましょう。
コードを書いてると値や式から型を取得したいと思うことが度々あります。
int a = 10; float b = 3.14f; // どんな型を返すんだっけ??? ??? c = a + b;
そういう時は C++11 から追加された decltype()
という機能を使用します。
decltype()
は式(値)を渡すことで『その式(値)の型』を返します。
int a = 42; decltype(a); // int 型を返す decltype(a) b = 10; // 型の変わりに使用することができる // typedef などで使用することもできる typedef decltype(a) a_type; float f = 3.14f; decltype(a + f) result = a + f; // decltype() には式も渡せる decltype(func(a, b)) result2 = func(a, b); // 関数も渡すことができる int const n = 42; // const 修飾子も加味した型を返す decltype(n); // const int
こんな感じです。
基本的に『decltype()
を定義したスコープから見える変数』はすべて参照する事が出来ます。
ですので以下のようにC++11 で追加された『後置戻り値型』でも decltype()
が使用できます。
template<typename T, typename U> auto plus(T a, U b) // 型情報だけでは演算結果の型を取得するのは難しいが // decltype() を利用すれば式から型を推論することができる ->decltype(a + b){ return a + b; }
decltype()
が具体的にどのような型を返すのかは以下のサイトがわかりやすく解説しています。
[まとめ]
そんな感じで値と型の話について書いてみました。
C++(に限らないけど)において『型情報』というのは重要なファクターになります。
特に昨今のテンプレートや auto
などを使用したコードでは『表面の型』が見えないことが多いです。
そういう時こそ『型』を意識してコードを書いていくことが C++ を学ぶ上で大切になっていきます。
動的型付け言語を普段書いている人にとって『型情報』というのはめんどくさい要因の一つだと思います。
しかし、『型による制約』によるリターンもとても大きいものではあります。
C++ 初心者の人はコードを書いてる時に少しだけ型について意識して書いてみると C++ への理解がより深まると思います。
【初心者C++er Advent Calendar 2016 11日目】C++ で型によるコンパイル時条件分岐技法まとめ
初心者C++er Advent Calendar 2016 11日目の記事です。
空いてるようなのでまた書きました。
さて、初心者用ということで型を使用した『コンパイル時条件分岐技法』を簡単に紹介してみます。
[注意]
本記事では C++11 で動作することを想定してるコードになります。
[多重定義]
まずは一番よく利用されている多重定義ですね。
C++ では引数の型を変えることで複数の関数を定義することが出来ます。
void print(int n){ std::printf("int 型:%d\n", n); } void print(float f){ std::printf("float 型:%f\n", f); } void print(char const* str){ std::printf("char const* 型:%s\n", str); } print(42); print(3.14f); print("homu"); /* output: int 型:42 float 型:3.140000 char const* 型:homu */
また、型以外に引数の数によっても複数の関数を定義することが出来ます。
int func(int n){ return n + n; } int func(int n, int m){ return n + m; } func(42); // 84 func(4, 6); // 10
まあ流石に C++ を書いてる人で多重定義を書いたことがない人はいないと思います。
[テンプレートの特殊化]
さて、次はテンプレートを使った条件分岐です。
いわゆる『テンプレートの特殊化』と言われるやつですね。
例えば、次のようなクラステンプレートがあったとします。
template<typename T> struct X{ int twice(){ return value + value; } T value; }; X<int> x{42}; x.twice(); // 84
この時にテンプレートが『任意の型』の場合に twice()
の実装を変えたいとします。
例えば、T = char
の場合は std::string
を返すようにしたい場合は X<char>
に対応するクラスを新しく定義します。
template<typename T> struct X{ int twice(){ return value + value; } T value; }; X<int> x{42}; x.twice(); // 84 // 新しくクラスを定義する // この template 引数は省略し template<> // ここに『任意の方』を記述する struct X<char>{ std::string twice(){ return std::string{value} + value; } // ここも T 型から char 型に書き直す char value; }; X<char> x2{'c'}; x2.twice(); // "cc"
こんな感じです。
まあこれは『こういうものだ!』という風に覚えるしかないですね。
また、『一部のテンプレート型に対して』も適用することが出来ます。
template<typename T, typename U> struct X{ }; template<typename U> // クラステンプレートの第一引数が int の場合のクラス struct X<int, U>{ };
ここで注意するのは template<>
の引数と X<int, U>
の引数ですね。
template<>
の引数は任意で構いませんが、X
に渡す引数の数は最初に定義したクラステンプレートと同じでなければなりません。
なので、例えば、次のように拡張することは出来ません。
template<typename T> // 引数の数が違うのでエラー struct X<int>{ };
こんな感じですね。
まあ使う機会は少ないかもしれませんが、覚えておくといざという時に利用できるかもしれません。
ちなみに標準ライブラリの悪名高い std::vector<bool>
もこのテンプレートの特殊化を利用してるクラスのひとつですね。
[SFINAE]
さて、SFINAE です。
「SFINAE (Substitution Failure Is Not An Errorの略称、スフィネェと読む)」は、テンプレートの置き換えに失敗した際に、即時にコンパイルエラーとはせず、置き換えに失敗した関数をオーバーロード解決の候補から除外するという言語機能である。
参照:任意の式によるSFINAE - cpprefjp C++日本語リファレンス
まあ、これ自体は難しい仕組みではなくて、例えば次のような関数テンプレートがあったとします。
template<typename T> typename T::value_type twice(T){ return value + value; }
これは、T 型に対して『::value_type
を要求してる』という関数になります。
なので、
struct Int{ using value_type = int; int value; }; // Int 型は ::value_type があるので問題なく動作する twice(Int{42});
というコードは問題ありませんが
struct Float{ float value; }; // Float は ::value_type を持っていないのでエラー twice(Float{42}); // int は ::value_type を持っていないのでエラー twice(42);
というような引数は ::value_type
がないのでエラーになります。
ここまではなんとなくわかると思います。
では次に『int
型を受け取る関数』を追加します。
template<typename T> typename T::value_type twice(T t){ return t.value + t.value; } // 新しく関数を追加 int twice(int n){ return n + n; } struct Int{ using value_type = int; int value; }; struct Float{ float value; }; // OK: T::value_type twice(T) が呼ばれる twice(Int{42}); // 84 // OK: int twice(int) が呼ばれる twice(42); // 84 // Error: どっちの関数にもマッチしない twice(Float{3.14f});
この時に int
型を引数に twice()
を呼び出した場合に関数テンプレートで『::value_type
がない』とエラーにならずに twice(int)
が呼び出されます。
つまり
と言うとおり『エラーにならないでオーバーロードから除外されるだけ』という挙動になります。
SFINAE はよく enable_if
などと組み合わせて利用されるためちょっとコードが複雑になってしまいますが、SFINAE 自体の仕組みはこれだけです。
そんなに難しくはないでしょ?
また、上記の例では『戻り値型に T::value_type
』を定義していましたが、他にも
template< typename T, // テンプレートのデフォルト引数として定義 typename Result = typename T::value_type > Result twice(T t){ return t.value + t.value; }
このように『テンプレートのデフォルト引数』として定義してみたり、
template<typename T> void print(T t, decltype(t.value)* = 0 /* メンバ変数 value にアクセスする */){ std::cout << t.value << std::endl; }
これならば『T がメンバ変数 value を持ってる』みたいな条件付けをすることも出来ます。
これにより『単純な型』だけではなく『より複雑な条件』で多重定義を行うことができるようになります。
このようなテクニックはいくかありますが、詳しくは"任意の式によるSFINAE"を参照してみてください。
[まとめ]
と、言う感じで簡単にコンパイル時条件分岐技法をまとめてみました。
正直 SFINAE に関してはぜんぜん紹介しきれてないですが、まあ初心者向けということで『こういうのもあるんだよー』ぐらいに思っておけばいいと思います。
SFINAE に関しては余裕があればまた別の Advent Calendar で記述したいと思います。
【C++ Advent Calendar 2016 13日目】C++ で楽しく Expression Templates しよう
C++ Advent Calendar 2016 13日目の記事です。
13日目が空いてるようなのでちょっと Expression Templates というマニアックなネタでも。
まあ C++ プロの方なら Expression Templates なんて余裕ですよね。
[注意]
本記事のコードは基本的には C++14 に準じているので、自環境で試したい場合は注意してください。
[Expression Templates とは]
Expression Templates とは wikibooks から引用すると
C/C++ において数学的な式を評価するありふれたやり方として、関数中に式をラップし、その関数ポインタを渡して、入力された数値群に対して適用するというものがある。この方法では、関数呼び出しと一時オブジェクト生成のオーバーヘッドが生じる。また、ソース中の式の位置が呼び出し元から非常に離れていることも頻繁であり、可読性と保守性に悪影響を与える。式テンプレート(Expression template)は、式をその場に展開することによって、関数ポインタを不要にし、式と呼び出し元をまとめることで問題を解決する。
式テンプレートには再帰的な型の合成(Recursive Type Composition)が用いられている。再帰的な型の合成では、少量のコードから複雑な型の階層を生じさせることができる。一方向の再帰的な型の合成では線形な型リスト(Type List)が生成される。二方向の再帰的な型の合成は二項式に対して有用であり、以下の例で用いている。メタプログラミングテクニックはデータのように型を合成することを可能にする。違いは、メタプログラムはコンパイル時に動作すること、またそれゆえデータではなく型に対してはたらくことだけである。
参照:More C++ Idioms/式テンプレート(Expression-template) - Wikibooks)
なるほどよくわからん。
と、言うことでこういう概念はすっ飛ばして、実際にコードを書いていってみます。
ちなみにこの記事は『Expression Templates とはなんぞやを解説する記事』ではなくて『C++14 でオレオレ Boost.Lambda を実装する』という記事になります。
[Boost.Lambda/Phoenix]
実際にコードを書いていく前に少し Boost.Lambda と Boost.Phoenix を解説します。
Boost.Lambda と Boost.Phoenix はまさに Expression Templates を利用して『ラムダ式(のようなもの)を定義するため』につくられたライブラリです。
ちなみに Boost.Lambda と Boost.Phoenix の2種類のライブラリが存在するがここでは Boost.Phoenix について説明します。
Boost.Phoenix は『プレースホルダ』と呼ばれるものを利用して式を定義していきます。
// この argN というのがプレースホルダになる // このプレースホルダはあとから評価した時の引数に着替えられる auto plus = arg1 + arg2 * arg3; // この時に arg1 は第一引数、arg2 は第二引数…という感じに置き換えられ // 最終的には 1 + 2 * 3 という式が評価される plus(1, 2, 3); // 7
このプレースホルダは何かというと単なる関数オブジェクトになり
arg1(1, 2, 3); // 1 arg2(1, 2, 3); // 2 arg3(1, 2, 3); // 3
という風に『第n番目の引数を返す』という挙動になります。
また、この他にも
// 値を返すだけの関数オブジェクトを返す auto _42 = val(42); // 引数の値を返すだけ _42(); // 42
という関数オブジェクトを返す val()
という関数も存在します。
この関数オブジェクトも先ほどのプレースホルダと組み合わせて使用できます。
// 関数オブジェクトから演算子を使って式を組み立てていく auto expr = arg1 + arg2 * val(42) expr(1, 2); // 85
こんな感じで Boost.Phoenix にはいろいろな関数オブジェクトが用意されており、それを組み合わせて式を定義していく感じになります。
ここで注意するのはすべて『関数オブジェクト』として扱うということです。
この点はかなり重要なので覚えておくとよいです。
それでは実際にこのような関数オブジェクトを実装してみます。
[val()
定義する]
まずは比較的な簡単な val()
の定義から。
まあこれは単に値を保持して返すだけのオブジェクトになります。
template<typename T> struct holder{ auto operator ()(...) const{ return value; } T value; }; template<typename T> holder<T> val(T t){ return {t}; } auto result = val(42)();
まあ値を保持する holder
クラスを用意して、それを生成するための val()
関数をヘルパ関数として定義してるだけですね。
この実装のキモとしては『実際の関数オブジェクトとして定義するクラス』と『その関数オブジェクトを返す関数』をわけて考えてることです。
これにより使用する側はクラステンプレートを意識する必要がありません。
あ、あと operator ()
が『可変長引数』を受け取っているのもポイントです。
これはあとから説明します。
ちなみに本筋とは関係ないところでコード量が増えてしまうので rvalue reference は省略して書いていきます。
まあ本物の C++ なら rvalue reference に対応するなど容易いはず
[プレースホルダオブジェクトを定義する]
次はプレースホルダです。
val()
と比較してちょっと複雑ですが、そこまで難しくはないです。
template<std::size_t Index> struct placeholder{ template<typename... Args> auto operator ()(Args... args) const{ // n番目の引数を受け取る実装に tuple を利用する return std::get<Index>(std::make_tuple(args...)); } }; const auto arg1 = placeholder<0>{}; const auto arg2 = placeholder<1>{}; const auto arg3 = placeholder<2>{}; arg1(1, 2, 3); // 1 arg2(1, 2, 3); // 2 arg3(1, 2, 3); // 3
こちらは operator ()
の引数を可変長テンプレート引数で受け取って std::tuple
と std::get
を利用して『任意の引数値を返す』という関数オブジェクトになっています。
また、これも『placeholder
クラス』と『arg1
, arg2
... オブジェクト』と切り分けて実装しています。
テンプレートを使用することでこういう『連番』のオブジェクトも比較的簡単に定義することが出来ますね。
[operator+
を定義]
さて、最後に関数オブジェクトを演算するための operator +
を定義します。
template<typename L, typename R> struct plus{ template<typename... Args> auto operator ()(Args... args) const{ return left(args...) + right(args...); } L left; R right; }; template<typename L, typename R> plus<L, R> operator +(L left, R right){ return {left, right}; }
まず、operator +
を定義し、その左辺値と右辺値を保持するクラスを定義します。
そして operator ()
ではその『左辺値と右辺値を評価』します。
ここがポイントですね。
つまり
関数オブジェクト + 関数オブジェクト
という式は
関数オブジェクト(引数) + 関数オブジェクト(引数)
という風に評価されます。
このように関数オブジェクトが再帰的に評価されることで arg1
や val
などの結果が最終的には『+
演算子として』処理されます。
はい、ここがポイントですね。
これにより
arg1 + arg2 + val(42) + arg1;
みたいな連続した式も問題なく展開されます。
この時にクラスが保持してる値が『関数オブジェクト』か『そうでない』かがキモになってきます。
今回定義した operator +
は左辺値と右辺値が『関数オブジェクトを受け取ること』を想定しているので『operator ()
』で評価します。
しかし、先ほど定義した val()
の holder
クラスは関数オブジェクトではなくて『値』を保持しているため『評価しないで』そのままの値を返します。
基本的にはこれの応用で他の演算子も定義すればおっけーです。
[完成]
以下が完成形になります。
#include <tuple> #include <iostream> namespace lambda{ template<typename T> struct holder{ auto operator ()(...) const{ return value; } T value; }; template<typename T> holder<T> val(T t){ return {t}; } template<std::size_t Index> struct placeholder{ template<typename... Args> auto operator ()(Args... args) const{ return std::get<Index>(std::make_tuple(args...)); } }; const auto arg1 = placeholder<0>{}; const auto arg2 = placeholder<1>{}; const auto arg3 = placeholder<2>{}; template<typename L, typename R> struct plus{ template<typename... Args> auto operator ()(Args... args) const{ return left(args...) + right(args...); } L left; R right; }; template<typename L, typename R> plus<L, R> operator +(L left, R right){ return {left, right}; } } int main(){ auto expr = lambda::arg1 + lambda::arg2; std::cout << expr(4, 2) << std::endl; auto plus3 = lambda::arg1 + lambda::val(3); std::cout << plus3(4) << std::endl; return 0; }
演算子は ADL で呼び出してるので namespace を追加してます。
とりあえず、これで完成ですね!
Expression Templates という概念はわかりづらいと思いますが、実際はこんな感じにパーツを組み合わせて式をつくっていくと考えればわかりやすいと思います。
C++11/14 でかなり実装も簡単になりましたしね。
また C++11 で言語レベルでラムダ式は追加されましたが、
filter(list, [](auto n){ return n < 0; });
よりも
filter(list, arg1 < val(0));
みたいに今回つくったラムダ式を使ったほうがコードがすっきりすると思います。
これは便利ですね。
ではみなさん、良いお年をー。
[これで終わりと思った?]
残念!まだ終わりではありません!!!
ここからが本番になります。
[C++14 で実装されたと多相ラムダと戻り値型の型推論]
さて、C++14 では新たに引数型を推論する多相ラムダと戻り値型を推論する強力な機能が追加されました。
[多相ラムダ]
ラムダ式の引数型を auto
にすることで関数テンプレートのように型推論を行ないます。
// 引数型に依存することなくラムダ式を定義することができる auto plus = [](auto a, auto b){ return a + b; }; plus(1, 2); // 3 plus(3.14f, 53.24f); // 56.38 plus(3.14f, 42); // 45.14 plus(3.14f, 42); // "homumami"
このように引数型に依存することなくラムダ式を定義することが出来ます。
多相ラムダを受け取ることができる型がほしいよね
[戻り値型の型推論]
C++14 では戻り値型を auto
とすることでコンパイラが式から型推論を行ってくれます。
template<typename T, typename U> auto plus(T a, U b){ // a + b の結果の型を推論してくれる return a + b; } plus(1, 2); // 3 plus(3.14f, 53.24f); // 56.38 plus(3.14f, 42); // 45.14 plus(3.14f, 42); // "homumami"
C++14 以前では戻り値型 decltype()
などを使用する必要がありましたが、C++14 ではそれすら必要なくなりました。
// C++11 時代は decltype() で戻り値型を定義する必要があった template<typename T, typename U> auto plus(T a, U b) ->decltype(a + b){ // a + b の結果の型を推論してくれる return a + b; }
decltype()
を使用した場合は『関数を定義するときに型が決定されてる必要』があったんですが、これにより『関数内で定義された型』も返すことができるようになりました。
// 関数内で定義したローカルクラスを返すこともできる auto make_homu(){ struct person{ std::string name; int age; }; return person{ "homu", 14 }; } auto data = make_homu(); data.name; // "homu" data.age; // 14
ただし、次のように『実行時によって異なる型を返す』ことは出来ないので注意してください。
auto func(int flag){ // Error: 同じ型を返さなければならない if( flag ){ return 0; } else { return "Error"; } }
[つまり]
もうお気づきの方もいると思いますが、これにより『ラムダ式』をそのまま返すことができるようになりました。
// C++ でクロージャっぽいものを定義 auto counter(){ int value = 0; // キャプチャした変数を書き換える場合は mutable を付属させる return [=]() mutable{ value += 1; return value; }; } count(); // 1 count(); // 2 count(); // 3
と、いうことで先ほどつくったプレースホルダなどをラムダ式を使って書き換えていきましょう
[val()
定義する 2]
まずは val
から。
これは引数をそのまま返すラムダ式を定義するだけなので簡単ですね。
template<typename T> auto val(T t){ return [=](...){ return t; }; }
はい、先ほど定義した holder
クラスは綺麗サッパリなくなって関数1つだけになりました。
すごいすっきりしましたね。
[プレースホルダオブジェクトを定義する 2]
続けてプレースホルダも定義してみます。
template<std::size_t Index> auto placeholder(){ return [](auto... args){ return std::get<Index>(std::make_tuple(args...)); }; } const auto arg1 = placeholder<0>(); const auto arg2 = placeholder<1>(); const auto arg3 = placeholder<2>();
はい、この通り placeholder
クラスではなくて placeholder
関数になりました。
また、多相ラムダを使用することで template
も省略出来ます。
こちらも実装がすっきりしましたね。
[operator+
を定義 2]
最後に operator +
ですね。
これも簡単に定義できます。
template<typename Left, typename Right> auto operator +(Left left, Right right){ return [=](auto... args){ return left(args...) + right(args...); }; }
完璧ですね…。
[まとめ]
と、言うことでまとめるとこんなコードになります。
#include <tuple> namespace lambda{ template<typename T> auto val(T t){ return [=](...){ return t; }; } template<std::size_t Index> auto placeholder(){ return [](auto... args){ return std::get<Index>(std::make_tuple(args...)); }; } const auto arg1 = placeholder<0>(); const auto arg2 = placeholder<1>(); const auto arg3 = placeholder<2>(); template<typename Left, typename Right> auto operator +(Left left, Right right){ return [=](auto... args){ return left(args...) + right(args...); }; } } // namespace lambda #include <iostream> int main(){ auto expr = lambda::arg1 + lambda::arg2; std::cout << expr(4, 2) << std::endl; auto plus3 = lambda::arg1 + lambda::val(3); std::cout << plus3(4) << std::endl; return 0; }
どうでしょう。
最初にクラスを使って実装したコードに比べてだいぶすっきりしたと思いませんか?
Expression Templates の話どこに行ったんだって感じですよね。
まあでも結果的に C++14 だとこんなコードが書けるようになるんですよ。
面白いですよね。
ちなみに C++14 + ラムダ式で実装した場合の欠点としては『constexpr
』として定義できないことです。
最初にクラスを使って実装した場合は戻り値型に constexpr
をつければコンパイル時に評価することが出来ます。
これは大変強力なのでどっちで実装するべきなのかは悩みどころですね…。
ただ、C++17 ではラムダ式が constexpr
が定義できるらしいのでそこもカバーできるようになると思います。
と、言うことで本記事は『Expression Templates を解説する』記事でもなく『オレオレ Boost.Lambda/Phoenix を実装する』記事でもなく『C++14 の多相ラムダと戻り値型の auto
が素晴らしい』という記事になります。
それではメリークリスマス。