为啥 c++11 随机分布是可变的?

Posted

技术标签:

【中文标题】为啥 c++11 随机分布是可变的?【英文标题】:Why are c++11 random distributions mutable?为什么 c++11 随机分布是可变的? 【发布时间】:2013-04-07 03:49:50 【问题描述】:

我认为 c++11 随机分布(例如uniform_int_distribution)生成的值仅取决于传递给operator() 的生成器的状态。但是,由于某种原因,operator() 的签名中没有 const 说明符。这是什么意思,我应该如何将分布作为函数参数传递?我以为我必须将它作为任何非可变参数传递:通过 const 引用,但现在我不确定。

【问题讨论】:

是的,我明白这是在c++标准中定义的,我不明白这是为什么。例如,均匀的int分布可以通过其左右边界完全参数化,正态分布可以通过均值和标准差进行参数化,离散分布可以通过个体概率等。所以可以在构建的那一刻完成,而且似乎没有理由允许更改分发实例(尤其是对于operator())。 分布是(概念上)有状态的函数(实际上有些可能以无状态的方式实现)。 @R.MartinhoFernandes 我会这么说。分布是一个数学概念,下一个分布产生的值不依赖于前一时刻产生的值。如果有人必须保持一些隐藏状态以避免一些计算或出于其他原因,他应该对字段使用可变说明符,但从逻辑上讲,分布必须是不可变的。 @Yuushi 不,状态改变了,它是我们传递给operator()的生成器的隐藏状态 @karlicoss 我以为你在使用 C++,而不是在做数学。在 C++ 中,分布是有状态的函数。 【参考方案1】:

另一个答案是:

这只是理论上的,但我想它不限于 const 的原因是允许实现者在需要时改变他们的实现。此外,它保持了一个更统一的接口——如果一些 operator() 是 const 而一些是非常量,这一切都会变得有点混乱。

这大部分是正确的,但它比泛型编程的上下文更深。 (正如@Calimo 所说,这留下了const 被省略只是“以防万一”的想法)。

考虑到这一点,我得出的结论是,问题转化为以下成员函数原则上 const与否真的取决于_UniformRandomNumberGenerator的实际类型。

template<typename _UniformRandomNumberGenerator>
result_type
operator()(_UniformRandomNumberGenerator& __urng)

在(通用)规范的这个级别上,这是不知道的,所以只有这样“[规范]允许实现者改变[内部状态]”并且为了通用性而这样做。

因此,constness 的问题是在编译时应该知道_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&lt;D&gt; 可以替代D。 (immutable&lt;D&gt; 的另一个名称可以是 conceptual&lt;D&gt;。)

例如,我已经用uniform_real_distribution 对此进行了测试,immutable 替换的速度几乎是原来的两倍(因为它复制/修改/丢弃了标称状态),但正如您指出的那样,它可以用于更多“如果这对您的设计很重要(我可以理解),则为“概念”上下文。

(还有另一个无关紧要的小优势,即您可以跨线程使用共享的不可变分布)


错误但说明性的代码如下:

为了说明实现 (2) 的难度,我将对 immutable&lt;std::uniform_int_distribution&gt; 进行 naive 特化,这对于某些用途几乎是正确的(或者非常不正确,具体取决于您询问的对象。 )

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;
;

这种无状态实现非常“高效”,但对于ab 的任意值(分布的限制)并非100% 正确。 如您所见,对于其他分布(包括连续分布),这条路径非常困难、棘手且容易出错,因此我不推荐它。


这主要是个人意见:情况可以改善吗?

是的,但只有一点点。

发行版可能有两个版本的operator(),一个没有-const(即&amp;),这是最佳的(当前一个),一个是const,它可以不修改状态.但是,尚不清楚它们是否必须具有确定性一致(即给出相同的答案)。 (即使回退到副本也不会给出与完整的可变分布相同的结果。)。但是,我认为这不是一条可行的道路(同意其他答案);您要么使用不可变版本,要么使用不可变版本,但不能同时使用两者。

我认为可以做的是有一个可变版本,但对 r 值引用有一个特定的重载 (operator() &amp;&amp;)。 这样可以使用可变版本的机制,但是可以省略现在“无用”的更新(例如重置)状态的步骤,因为特定实例将不再被使用。这样在某些情况下可以节省一些操作。

这样,上面描述的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);
   
;

【讨论】:

【参考方案2】:

一开始我误解了这个问题,但是现在我明白了,这是一个很好的问题。对 g++ 的&lt;random&gt; 的实现源代码进行了一些挖掘(为了清楚起见,省略了一些内容):

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,它本身只是一个包含两个整数值的嵌套结构——实际上是一个范围。 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,这一切都会变得有点混乱。

但是,为什么他们不简单地将它们设为 const 并让实现者使用mutable 我不确定。很可能,除非附近有人参与了这部分标准化工作,否则您可能无法得到很好的答案。

编辑:正如 MattieuM 指出的那样,mutable 和多个线程不能很好地协同工作。

除了有点有趣之外,std::normal_distribution 一次生成两个值,缓存一个(因此是_M_saved)。它定义的operator&lt;&lt; 实际上让您在下次调用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

【讨论】:

@R.MartinhoFernandes 对于std::uniform_int_distribution 之类的东西,您可以让它每次都发布一个新的发行版,而且它会非常好,即使在实施时也是如此。从分布中抽取数字不应该(理论上)以任何方式修改分布本身。如果我从 mu=0 和 sigma=1 的正态分布中抽取一个数字,则该分布在此之后仍然是 mu=0 和 sigma=1 的正态分布。 尽管名称相同,但 C++ 中的分布与数学中的分布不同。这是你必须接受的。事实是,一些 C++ 发行版具有可变状态并且该状态是可观察的(但观察它可能并不容易,因为我们谈论的是随机性和概率):编写假设没有可观察状态的代码会导致输出分布不均。并且mutable 绝不应该用于隐藏可观察状态。 @Yuushi:该标准拒绝隐藏可变性,因为它旨在保证可以从多个线程安全地调用对象上的const 操作。这就是为什么 C++11 标准现在禁止 COW。 @karlicoss:我不确定整个库中的每一个单次访问,​​但我知道这是一个设计目标,它已针对所有 STL 容器和basic_string 类。 @R.MartinhoFernandes “尽管名称相同,但 C++ 中的分布与数学中的分布不同。这是你必须接受的东西”,OP 询问为什么会这样,所以这并不能真正回答问题(这个答案似乎认为这是一个实现细节,有点弱)。

以上是关于为啥 c++11 随机分布是可变的?的主要内容,如果未能解决你的问题,请参考以下文章

为啥有时 IEnumerable<T> 中的元素是可变的,而有时它们是不可变的? [复制]

为啥在 Rust 中将 const 引用直接转换为可变引用无效?

为啥链接生命周期仅与可变引用有关?

PHP 的可变长度参数 `...` 标记应该被称为啥?

[C++11 模板的改进] --- 可变参数模板

C++11 ——— 可变参数模板