背景
いま、基本型(e.g. int32_t
, uint64_t
, double
, float
)のどれが来ても対応できるようなジェネリックなコードを書いているとします。そして、そのコードにランダムな値を渡したいとします(例えばテストをしているとか)。
template<typename T>
bool is_ok(T rnd);
ランダムな値はどう生成したらいいでしょう。
標準ライブラリの<random>
では、整数型はuniform_int_distribution
、浮動小数点数型はuniform_real_distribution
で、名前が異なります。
できれば以下のようなランダムな値を返すジェネリックな関数を書きたいものですが、使うクラスの名前が違うのでint
とdouble
で同じ実装は書けません。何らかの方法で実装を切り替える必要があります。
template<typename T, typename RNG>
T generate_random(RNG&& rng)
{
// ...?;
}
部分特殊化・オーバーロード
関数templateは部分特殊化ができません。
template<typename RNG>
int generate_random<int, RNG>(RNG&& rng);
template<typename RNG>
double generate_random<double, RNG>(RNG&& rng); // できない
ではオーバーロードではどうでしょう。
template<typename RNG>
void generate_random(RNG&& rng, int& value);
template<typename RNG>
void generate_random(RNG&& rng, double& value);
これを全ての基本型について書くのはあまりにも面倒です。
そうでなくとも、使う側からすれば書き込み先を先に用意する必要があり、しかも書き込み可能にしなければならないインターフェースはそんなに使い勝手は良くないです。
double value;
generate_random(rng, value);
// vs
const auto value = generate_random<double>(rng);
では、無理やり部分特殊化をできるようにするのはどうでしょう。クラステンプレートは部分特殊化できるので、クラスのメンバを呼び出すようにすれば部分特殊化が可能です。
template<typename T>
struct generate_random_impl
{
template<typename RNG>
static T invoke(RNG&& rng);
};
// generate_random_implを特殊化する……
template<typename T, typename RNG>
T generate_random(RNG&& rng)
{
return generate_random_impl<T>::invoke(rng);
}
しかし、これも結局目的の型について全ての部分特殊化を用意する必要があることに変わりありません。
そもそも今回の目的は「個々の型について別の実装を(特殊化の本来の意味)」ではなく、「特定の種の型について分岐する」という話なので特殊化で解決するのはそもそも少し筋が悪いでしょう。
浮動小数点数型ならこう、整数型ならこう、というだけの話なのに、それをそのまま実装できないものでしょうか。
タグディスパッチ
例えば、タグディスパッチという手法があります。これは、特別な構造体を作ってオーバーロード解決につかうというものです。
といっても構造体に特別な実装は必要ありません。というか実装がそもそも必要ありません。別の型であることがわかればそれで事足りるので、空でかまいません(もちろん、値を持っていても構いません。型しか見ないので)。
たとえば、std::true_type
とstd::false_type
があります。これらは違う型なのでオーバーロード解決に使うことが出来ます。整数型かそうでないかをこれを使って分岐してみましょう。
template<typename T, typename RNG>
T generate_random_impl(RNG&& rng, std::true_type) // 整数型ならtrue_typeを渡す
{
std::uniform_int_distribution<T> uni(0, 100);
return uni(rng);
}
template<typename T, typename RNG>
T generate_random_impl(RNG&& rng, std::false_type) // それ以外
{
std::uniform_real_distribution<T> uni(0, 100);
return uni(rng);
}
template<typename T, typename RNG>
T generate_random(RNG&& rng)
{
return generate_random_impl<T>(std::forward<RNG>(rng), std::is_integral<T>{});
}
std::is_integral
には色々な整数型への特殊化が用意されていて、整数値型の場合はstd::true_type
から、そうでない場合はstd::false_type
から派生しています。なのでその値を作って渡せば、オーバーロードを介して適切な方を選ぶことが出来ます。
これは動きますが、問題があります。整数型でないものは全て浮動小数点型だと思っている点です。この関数には整数型か浮動小数点型のどちらかしか渡してほしくないので、例えば大元の関数でそれを確認するなどの対策をとるべきでしょう。
template<typename T, typename RNG>
T generate_random(RNG&& rng)
{
static_assert(std::is_integral_v<T> || std::is_floating_point_v<T>,
"generate_random accept only Integer or Real values");
return generate_random_impl<T>(std::forward<RNG>(rng), std::is_integral<T>{});
}
もちろん、以上の解決策は二者択一だからできることです。3つ、4つで分岐しようとするなら、複数回上記のディスパッチを行うか、true_type
とfalse_type
をやめて、タグに使える構造体をいくつか自作して、それを返すtraitクラスを用意する必要があるでしょう。
例として、標準ライブラリでは、iterator_category
がその用途で使われています。iterator_traits<T>::iterator_category
でタグ型を取り出すことができます。
例えば2つのイテレータ間の要素数を計算するstd::distance
関数では、std::random_access_iterator
が来たときだけ引き算で値を返し、そうでなければ2つのイテレータが一致するまでインクリメントするという実装になっています。
SFINAEとenable_if
今回に関しては、よりよいやり方があります。SFINAEとstd::enable_if
です。
SFINAEはオーバーロード解決の途中に実体化に失敗するtemplate
があっても、他にマッチするオーバーロードがあればコンパイルエラーにはならないというルールです。一つちゃんと解決できるオーバーロードがあるなら、他のオーバーロードを使おうとした時にどうなろうと、ちゃんと動くものを使って動いて欲しいですよね? SFINAEは名前と使われ方が奇妙なので恐れられていますが(要出典)、単にこれだけのことです。
また、std::enable_if
はコンパイル時条件が真の時はenable_if::type
を持つものの、偽の時は持たないという型です。なので、中でstd::enable_if<条件>::type
を使っている関数は、条件が真でないと実体化に失敗する関数になります。なにせ条件が偽なら、そんな型は存在しないので!
これらを使うと、条件式が偽のときはtemplate
の実体化に失敗するのでオーバーロード解決の候補にならず、コンパイラにスルーされる関数を作ることが出来ます。
template<typename T, typename RNG,
std::enable_if_t<std::is_integral_v<T>, std::nullptr_t> = nullptr>
T generate_random(RNG&& rng)
{
std::uniform_int_distribution<T> uni(0, 100);
return uni(rng);
}
template<typename T, typename RNG,
std::enable_if_t<std::is_floating_point_v<T>, std::nullptr_t> = nullptr>
T generate_random(RNG&& rng)
{
std::uniform_real_distribution<T> uni(0, 100);
return uni(rng);
}
上の関数はstd::is_integral_v<T>
がtrue
の時だけ、3つ目のtemplate
引数が定義されます。そうでない場合定義されず、存在しない型を受け取ることになり、実体化に失敗します。
なので、もしdouble
がgenerate_random
に渡された場合、コンパイラは例えば先に上の関数にdouble
を入れてみて失敗し(3つめのtemplate
引数が定義できないため)、次に下の関数に入れて成功してお祝いをするでしょう。
ちなみにenable_if
はtemplate
引数以外にも使うことができます。例えば引数や、
template<typename T, typename RNG>
T generate_random(RNG&& rng, std::enable_if_t<std::is_integral_v<T>, std::nullptr_t> = nullptr)
{
std::uniform_int_distribution<T> uni(0, 100);
return uni(rng);
}
戻り値でもokです。
template<typename T, typename RNG>
std::enable_if_t<std::is_integral_v<T>, T>
generate_random(RNG&& rng)
{
std::uniform_int_distribution<T> uni(0, 100);
return uni(rng);
}
enable_if::type
は第2template
引数に渡した型になるので、時と場合に応じて適切な型を与えましょう。
普通enable_if
の存在は呼び出し側に意識させたくないので、デフォルト引数を渡すのが主流ですが、そのときに渡す値が一つしか存在しないという点で便利なnullptr_t
が使われています。
ちなみにC++11以前は関数にデフォルトテンプレート引数を渡せなかったため、引数か戻り値のどちらかしか使われて来ませんでした。
C++17なら?
さて、上記まではC++11でも(書き方は少し制限されますがC++98でも)可能な手法ばかりでしたが、最新規格ではどうできるでしょう? 新機能はプログラマを幸せにするために追加されるものなので、こういうことをするのも簡単になっているはずですよね?
C++17ではconstexpr if
がコア言語に入ったので、以下のようなことができます。
template<typename T, typename RNG>
T generate_random(RNG&& rng)
{
if constexpr (std::is_integral_v<T>)
{
std::uniform_int_distribution<T> uni(0, 100);
return uni(rng);
}
else if constexpr (std::is_floating_point_v<T>)
{
std::uniform_real_distribution<T> uni(0, 100);
return uni(rng);
}
else
{
static_assert(false_v<T>);
}
}
if constexpr (条件)
で、条件に合わないif
節の実体化を抑制することが可能です。
コンパイラは、関数template
が何か具体的な型をあてがわれて呼ばれた時に、ジェネリックなコードに型引数を代入して関数の実際の実装を作り出します。constexpr if
は、コンパイラが実装を作り出す時に、一部分を無視させる方法です。
もしここで普通のif
を使っていたらどうなるでしょう。コンパイラはtemplate
引数に渡された型のそれぞれについて関数全体を実体化しますから、int
が来た場合でもstd::uniform_real_distribution<int>
を実体化しようとします。そしてエラーになるでしょう。
しかしconstexpr if
は条件にそぐわない部分の実体化をしないので、難を逃れることができます。
(※最後のfalse_v
は、static_assert
が「else
節においてtemplate
の実体化をした」場合のみコンパイルエラーにするために必要なものです。詳しくは『江添亮の詳説C++17』を読んでください。)
あとがき
古典的な手法ですが、タグディスパッチやSFINAEに関してあまり複雑にならない例を見つけた気がしたので書いてみました。
これを機に興味を持った人が現れてくれれば幸いです。
おまけ
値の範囲が関係ないなら、そんな変な技法を習得しなくても、今回使ってる型の値は要するにビット列なんだからビットをランダムに埋めればいいじゃん! という発想のコードが以下です。
template<typename T, typename RNG>
T generate_random(RNG&& rng)
{
T value;
std::uint8_t* iter = reinterpret_cast<std::uint8_t*>(std::addressof(value));
std::uniform_int_distribution<std::uint8_t> uni(0x00u, 0xFFu);
for(std::size_t i=0; i<sizeof(T); ++i)
{
*(iter++) = uni(rng);
}
return value;
}
残念ながらこれは浮動小数点数については意図していた通りに動きません。最初に想定していたのは普通の数(今回は適当に1から100にしましたが)がランダムに返ってくるというものですが、浮動小数点数には普通の数値でない特別な値があります。Inf
とNaN
です。NaNの表現は「指数部ビットが全て1で仮数部ビットが非ゼロ」なので、それなりの確率で出現します。Inf
は仮数部がゼロでなければならないので、だいぶ確率が低いです。