最近準備刷題,打算簡單封裝下隨機數生成器,方便產生測試數據。C++11的STL提供了很多分布類型,我比較常用的是均勻分布,均勻分布的值有兩種類型,一類是整數,另一類是浮點數,STL根據值的類型定義了兩個函數 std::uniform_int_distribution
和 std::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) {// lambdadistribute_ = [=]() {// 類模板特化 默認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){// downscalingconst __uctype __uerange = __urange + 1; // __urange can be zeroconst __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 + lowwherehigh in [0, urange / (urngrange + 1)]andlow in [0, urngrange].*/__uctype __tmp; // wraparound controldo{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;
}
本文博客地址