封裝不同類别範本的隨機數生成器

kykx發表於2021-02-21

最近準備刷題,打算簡單封裝下隨機數生成器,方便產生測試資料。C++11的STL提供了很多分佈型別,比較常用的是均勻分佈,均勻分佈的值有兩種型別,一類是整數,另一類是浮點數,STL根據值的型別定義了兩個函式 std::uniform_int_distributionstd::uniform_real_distribution 。為了方便使用,我期望在使用的時候通過函式模板的實參推匯出要生成的數值型別,而不是顯式指定要生成的數值型別。

判斷模板實參型別

上面這個需求很簡單,最開始想到的方式是對模板實參推斷的型別進行判斷,根據判斷結果做不同的處理。

#include <random>
#include <type_traits>
#include <functional>
#include <limits>

std::default_random_engine e;

template<typename T_>
void Randoms(size_t counts, T_ min, T_ max) {
    std::function<T_()> distribute_;
    if (std::is_integral<T_>::value) {
        // lambda
        distribute_ = [=]() {
            // 類别範本特化 預設result_type int;
            std::uniform_int_distribution<> u(min, max);
            return u(e);
        };
    } else {
        // 類别範本特化 預設result_type double;
        std::uniform_real_distribution<> u(min, max);
        // std::bind
        // 成員函式返回值預設型別為double;
        using type_ = double (std::uniform_real_distribution<>::*)
                (std::default_random_engine &);
        distribute_ = std::bind(
                static_cast<type_>(&std::uniform_real_distribution<>::operator()),
                &u,
                e);
    }
    for (int i = 0; i < counts; ++i) {
        std::cout << distribute_() << " ";
    }
    std::cout << std::endl;
}


int main(int argc, char **argv) {
    Randoms(5,
            std::numeric_limits<std::size_t>::min(),
            std::numeric_limits<std::size_t>::max());
    Randoms(5, -0.5, 0.5);
    return 0;
}

從這個測試例項的結果來看,產生的數值遠遠超過了預設的 int 型別所能表示的數值,然後去看了看具體實現。下面程式碼是MinGW中GCC的實現版本,從實現中可以看出,均勻分佈的類别範本過載了函式呼叫運算子,接收兩個引數,一個是均勻分佈的隨機數生成器,第二個是引數型別。首先定義了三個型別別名:

  • 生成器的結果型別 _Gresult_type
  • 無符號的結果型別 __utype,由於類别範本推斷的結果型別是 int_utype 的實際型別就是 unsigned int
  • 生成器的結果型別和無符號的結果型別的共同型別 __uctype, 該型別只有當 _Gresult_type__utype 可以相互轉換才存在,這個過程實際就是型別轉換。

隨後使用 __uctype型別別名又定義了下面幾個關鍵的變數,簡單來看,生成器相關的屬性值(如最大、最小值等)與隨機數引擎有關,只要隨機數引擎不變,生成器的屬性值應該就不會變(至於屬性值到底變不變以及是否與隨機數種子有關等,待以後有時間再看看那部分原始碼怎麼寫的,目前就按不變來理解)。

  • __urngmin 表示生成器的最小值,不會改變;
  • __urngmax 表示生成器的最大值,不會改變;
  • __urngrange 表示生成器的數值範圍,不會改變;
  • __urange 表示引數型別的數值範圍,在放大操作時會隨著遞迴進行改變;
  • __ret 表示返回結果的一部分,加上引數範圍的最小值即為最終的隨機數結果。

當生成器的範圍超過了引數型別的範圍,那麼生成器生成的數值則可能超過引數型別的範圍,這時候就需要進行縮小操作。縮小操作首先計算出縮小因子,然後根據縮小因子計算出縮小後符合引數型別的生成器的最大值,只要生成器生成的值超過了允許的最大值則繼續生成下一個,直至生成符合要求的數值,最後將生成器的數值按比例縮小。

當生成器的範圍低於引數型別的範圍,那麼就無法生成超過生成器範圍,低於引數型別範圍的數值,這時候就需要進行放大操作。放大操作是一個迴圈遞迴,遞迴終止的條件則是生成器的範圍大於等於引數型別的範圍,當生成器生成的值超過了引數型別的範圍,說明生成的數值不正確,需要繼續重新生成。根據隨機數計算公式可以看到,high 的區間是 [0,urange / (urngrange + 1)]low 的區間是 [0, urngrange],這兩個區間左右兩側都是閉區間,那麼根據 (urngrange + 1) * high + low 計算的區間則是[0,urange + low],因此生成的數值可能比 urange大,這個可能性被第一個迴圈條件處理了。從迴圈的條件看還有一個判斷條件,這個條件還沒太理解,初步猜測與數值溢位有一定關係。如果 __tmp 已經處於最大值,此時再加上一個非0的隨機數,那麼則可能超過 __ret 本身所能表示的範圍,導致溢位。

template <typename _IntType>
template <typename _UniformRandomNumberGenerator>
typename uniform_int_distribution<_IntType>::result_type
uniform_int_distribution<_IntType>::
operator()(_UniformRandomNumberGenerator &__urng,
           const param_type &__param)
{
  typedef typename _UniformRandomNumberGenerator::result_type
      _Gresult_type;
  typedef typename std::make_unsigned<result_type>::type __utype;
  typedef typename std::common_type<_Gresult_type, __utype>::type
      __uctype;
  const __uctype __urngmin = __urng.min();
  const __uctype __urngmax = __urng.max();
  const __uctype __urngrange = __urngmax - __urngmin;
  const __uctype __urange = __uctype(__param.b()) - __uctype(__param.a());
  __uctype __ret;
  if (__urngrange > __urange)
  {
    // downscaling
    const __uctype __uerange = __urange + 1; // __urange can be zero
    const __uctype __scaling = __urngrange / __uerange;
    const __uctype __past = __uerange * __scaling;
    do
      __ret = __uctype(__urng()) - __urngmin;
    while (__ret >= __past);
    __ret /= __scaling;
  }
  else if (__urngrange < __urange)
  {
    // upscaling
    /*
      Note that every value in [0, urange]
      can be written uniquely as
      (urngrange + 1) * high + low
      where
      high in [0, urange / (urngrange + 1)]
      and
      low in [0, urngrange].
    */
    __uctype __tmp; // wraparound control
    do
    {
      const __uctype __uerngrange = __urngrange + 1;
      __tmp = (__uerngrange * operator()(
          __urng, 
          param_type(0, __urange / __uerngrange)));
      __ret = __tmp + (__uctype(__urng()) - __urngmin);
    } while (__ret > __urange || __ret < __tmp);
  }
  else
    __ret = __uctype(__urng()) - __urngmin;
  return __ret + __param.a();
}

std::enable_if 模板元方法

std::enable_if 的定義如下:

template< bool B, class T = void >
struct enable_if;

std::enable_if 實現的功能是根據類别範本引數 B 來決定是否定義型別 T 。它是一種元函式,利用 SFINAE 根據型別特徵有條件地從過載解析中刪除函式,併為不同的型別特徵提供單獨的函式過載和特化 std :: enable_if 可用作附加函式引數(不適用於運算子過載),返回型別(不適用於建構函式和解構函式)或用作類别範本或函式模板引數。參考 https://en.cppreference.com/w/cpp/types/enable_if

SFINAE"Substitution Failure Is Not An Error" 的簡寫,表示替換失敗不是錯誤,這個規則在函式模板的過載解析中經常被使用,當特化發生替換失敗時,這個特化會被從函式集中刪除掉,而不是導致一個編譯錯誤。

根據這個原則,只要通過返回值進行區分,就能實現這兩個函式的封裝了,非常簡單明瞭。

#include <random>
#include <type_traits>
#include <functional>
#include <limits>
#include <algorithm>

class Randoms {
public:
    template<typename T_>
    std::vector<T_> operator()(std::size_t counts, T_ min, T_ max) {
        std::vector<T_> vec;
        vec.reserve(counts);
        for (int i = 0; i < counts; ++i) {
            vec.push_back(generate_(min, max));
        }
        return std::move(vec);
    }

private:
    std::default_random_engine e;

    template<typename T_>
    std::enable_if_t<std::is_integral<T_>::value, T_>
    generate_(T_ min, T_ max) {
        std::uniform_int_distribution<T_> u(min, max);
        return u(e);
    }

    template<typename T_>
    std::enable_if_t<std::is_floating_point<T_>::value, T_>
    generate_(T_ min, T_ max) {
        std::uniform_real_distribution<T_> u_real(min, max);
        return u_real(e);
    }
};


int main(int argc, char **argv) {
    auto int_result = Randoms()(5,
                                std::numeric_limits<std::size_t>::min(),
                                std::numeric_limits<std::size_t>::max());
    auto real_result = Randoms()(5, -0.5, 0.5);

    for (const auto &ci : int_result) {
        std::cout << ci << " ";
    }
    std::cout << std::endl;

    std::for_each(real_result.cbegin(),
                  real_result.cend(),
                  [](const auto &cr) { std::cout << cr << " "; });
    std::cout << std::endl;
    return 0;
}

本博文地址

相關文章