LoginSignup
2
4

More than 5 years have passed since last update.

基本的な型のランダムな値を返す

Posted at

背景

いま、基本型(e.g. int32_t, uint64_t, double, float)のどれが来ても対応できるようなジェネリックなコードを書いているとします。そして、そのコードにランダムな値を渡したいとします(例えばテストをしているとか)。

template<typename T>
bool is_ok(T rnd);

ランダムな値はどう生成したらいいでしょう。
標準ライブラリの<random>では、整数型はuniform_int_distribution、浮動小数点数型はuniform_real_distributionで、名前が異なります。

できれば以下のようなランダムな値を返すジェネリックな関数を書きたいものですが、使うクラスの名前が違うのでintdoubleで同じ実装は書けません。何らかの方法で実装を切り替える必要があります。

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_typestd::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_typefalse_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引数が定義されます。そうでない場合定義されず、存在しない型を受け取ることになり、実体化に失敗します。

なので、もしdoublegenerate_randomに渡された場合、コンパイラは例えば先に上の関数にdoubleを入れてみて失敗し(3つめのtemplate引数が定義できないため)、次に下の関数に入れて成功してお祝いをするでしょう。

ちなみにenable_iftemplate引数以外にも使うことができます。例えば引数や、

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にしましたが)がランダムに返ってくるというものですが、浮動小数点数には普通の数値でない特別な値があります。InfNaNです。NaNの表現は「指数部ビットが全て1で仮数部ビットが非ゼロ」なので、それなりの確率で出現します。Infは仮数部がゼロでなければならないので、だいぶ確率が低いです。

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4