最近準備刷題,打算簡單封裝下隨機數生成器,方便產生測試資料。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) {
// 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;
}