我本以为由c++11随机分布(uniform_int_distribution
等)生成的数值只取决于传递给operator()
的生成器的状态。然而,由于某种原因,在operator()
的签名中没有const
说明符。这是什么意思,我该如何将分布作为函数参数传递?我原本认为必须将它作为任何不可变参数通过const引用传递,但现在我不确定了。
一开始我误解了问题,但是现在我理解了,这是一个好问题。对于g++的<random>
实现源码进行了一些研究,以下是部分内容(为了清晰起见略去了一些细节):
template<typename _IntType = int>
class uniform_int_distribution
{
struct param_type
{
typedef uniform_int_distribution<_IntType> distribution_type;
explicit
param_type(_IntType __a = 0,
_IntType __b = std::numeric_limits<_IntType>::max())
: _M_a(__a), _M_b(__b)
{
_GLIBCXX_DEBUG_ASSERT(_M_a <= _M_b);
}
private:
_IntType _M_a;
_IntType _M_b;
};
public:
/**
* @brief Constructs a uniform distribution object.
*/
explicit
uniform_int_distribution(_IntType __a = 0,
_IntType __b = std::numeric_limits<_IntType>::max())
: _M_param(__a, __b)
{ }
explicit
uniform_int_distribution(const param_type& __p)
: _M_param(__p)
{ }
template<typename _UniformRandomNumberGenerator>
result_type
operator()(_UniformRandomNumberGenerator& __urng)
{ return this->operator()(__urng, this->param()); }
template<typename _UniformRandomNumberGenerator>
result_type
operator()(_UniformRandomNumberGenerator& __urng,
const param_type& __p);
param_type _M_param;
};
如果我们忽略掉所有的_
,我们可以看到它只有一个成员参数,param_type _M_param
,它本身只是一个包含2个整数值的嵌套结构体——实际上是一个范围。这里仅声明了operator()
,并未定义。进一步挖掘可找到其定义。为避免在此发布所有相当丑陋(而且相当冗长)的代码,简单地说,在此函数内没有任何变异。事实上,添加const
到定义和声明中将会编译通过。std::normal_distribution
的实现,我们会发现:template<typename _RealType>
template<typename _UniformRandomNumberGenerator>
typename normal_distribution<_RealType>::result_type
normal_distribution<_RealType>::
operator()(_UniformRandomNumberGenerator& __urng,
const param_type& __param)
{
result_type __ret;
__detail::_Adaptor<_UniformRandomNumberGenerator, result_type>
__aurng(__urng);
//Mutation!
if (_M_saved_available)
{
_M_saved_available = false;
__ret = _M_saved;
}
//Mutation!
这只是一种理论推测,但我想之所以没有限制于const
,是为了让实现者在需要时可以更改自己的实现。此外,它保持了更统一的接口 - 如果有些operator()
是const
而有些不是,那就会变得有点混乱。
然而,他们为什么不简单地将它们设为const,让实现者使用mutable
我不确定。很可能,除非这里有人参与了标准化工作的这部分内容,否则你可能无法得到一个好的答案。
编辑: 正如MattieuM指出的,mutable
和多线程不兼容。
只是一个小有趣的细节,std::normal_distribution
一次生成两个值,缓存其中一个(因此有_M_saved
)。它定义的 operator<<
实际上允许您在下一次调用operator()
之前查看该值:
#include <random>
#include <iostream>
#include <chrono>
std::default_random_engine eng(std::chrono::system_clock::now().time_since_epoch().count());
std::normal_distribution<> d(0, 1);
int main()
{
auto k = d(eng);
std::cout << k << "\n";
std::cout << d << "\n";
std::cout << d(eng) << "\n";
}
这里的输出格式为mu sigma nextval
。
std::uniform_int_distribution
这样的东西,你 可以 每次创建一个新分布,实现也没有问题。从分布中抽取数字在理论上不应以任何方式修改分布本身。如果我从均值为0,标准差为1的正态分布中抽出一个数字,那么该分布仍将保持均值为0,标准差为1的正态分布。 - Yuushimutable
不应用于隐藏可观察状态。 - R. Martinho Fernandesbasic_string
类中得到了执行。 - Matthieu M.const
只是“以防万一”。)_UniformRandomNumberGenerator
的实际类型。template<typename _UniformRandomNumberGenerator>
result_type
operator()(_UniformRandomNumberGenerator& __urng)
_UniformRandomNumberGenerator
是否能够生成足够的随机性(位数),以便分布产生样本抽取。template<typename _URG, typename = std::enable_if<not has_enough_randomness_for<_URG, result_type>::value > >
result_type
operator()(_UniformRandomNumberGenerator& __urng){..statefull impl..}
template<typename _URG, typename = std::enable_if<has_enough_randomness_for<_URG, result_type>::value > >
result_type
operator()(_UniformRandomNumberGenerator& __urng) const{..stateless impl...}
在这里,has_enough_randomness_for
是一个想象中的布尔元函数,可以判断特定实现是否可以无状态。
然而,还有另一个障碍,一般来说,实现是否有状态取决于分布的运行时参数。 但由于这是运行时信息,因此无法作为类型系统的一部分传递!
正如您所看到的,这又引发了另一个问题。虽然理论上分布的constexpr
参数可以检测到这一点,但我完全理解委员会停在这里。
如果您需要一个不可变的分布(例如为了“概念上”的正确),则可以通过付出代价轻松实现:
(1) 可能非常低效,而 (2) 很可能效率稍低且极其难以正确实现。
由于 (2) 在一般情况下几乎不可能做到正确,即使做到了,它也会有些低效,因此我只会展示如何实现一个能够正常工作的无状态分布:
template<class Distribution>
struct immutable : Distribution{
using Distribution::Distribution;
using Distribution::result_type;
template<typename _URG> result_type operator()(_URG& __urng) const{
auto dist_copy = static_cast<Distribution>(*this);
return dist_copy(__urng);
}
// template<typename _URG> result_type operator()(_URG& __urng) = delete;
};
immutable<D>
是D
的替代品。(immutable<D>
的另一个名称可以是conceptual<D>
。)uniform_real_distribution
进行了测试,发现immutable
替代品几乎慢了一倍(因为它复制/修改/丢弃名义状态),但正如您所指出的那样,如果在设计中重视更“概念性”的上下文,则可以使用它(我可以理解)。以下是错误但具有说明性的代码:
为了说明(2)有多么困难,我将创建一个天真的特化版本immutable<std::uniform_int_distribution>
,对于某些用途几乎正确(或者根据不同的人会非常不正确)。
template<class Int>
struct immutable<std::uniform_int_distribution<Int>> : std::uniform_int_distribution<Int>{
using std::uniform_int_distribution<Int>::uniform_int_distribution;
using std::uniform_int_distribution<Int>::result_type;
template<typename _URG> result_type operator()(_URG& __urng) const{
return __urng()%(this->b() - this->a()) + this->a(); // never do this ;) for serious stuff, it is wrong in general for very subtle reasons
}
// template<typename _URG> result_type operator()(_URG& __urng) = delete;
};
operator()
,一个是非const
(即&
),它是最优的(当前版本),另一个则是const
的,尽可能不修改状态。然而,它们是否需要确定性一致(即给出相同的答案)还不清楚。(即使回退到复制,也无法像完整的可变分布那样给出相同的结果。)然而,我认为这不是一条可行的道路(与其他答案一致);你要么使用一个不可变版本,要么使用一个不可变版本,但不能同时使用两个。operator() &&
)。这样就可以使用可变版本的机制,但现在“无用”的更新步骤(例如重置)可以省略,因为该特定实例将永远不会再次使用。这样,在某些情况下可以节省一些操作。immutable
适配器就可以这样编写并利用语义:template<class Distribution>
struct immutable : Distribution{
using Distribution::Distribution;
using Distribution::result_type;
template<typename _URG> result_type operator()(_URG& __urng) const{
auto dist_copy = static_cast<Distribution>(*this);
return std::move(dist_copy)(__urng);
// or return (Distribution(*this))(__urng);
}
};
operator()
)。 - karlicossoperator()
的生成器的隐藏状态。 - karlicoss