37

C++11 のランダム分布 (uniform_int_distributionたとえば ) によって生成される値は、 に渡されるジェネレーターの状態のみに依存すると考えましたoperator()。ただし、何らかの理由constで、の署名に指定子がありませんoperator()。それはどういう意味ですか?関数パラメーターとして分布を渡すにはどうすればよいですか? 私はそれを変更不可能なパラメーターとして渡さなければならないと思っていました: const 参照によって、しかし今はわかりません。

4

2 に答える 2

23

最初は質問を誤解していましたが、今では理解できました。良い質問です。for 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;
};

すべての を目を細めて_見ると、メンバー パラメータ が 1 つしかないことがわかります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、すべてが少し面倒になります。

しかし、なぜそれらを単純に const にせず、実装者に利用させなかったのmutableかはわかりません。このあたりの誰かが標準化作業のこの部分に関与していない限り、これに対する適切な回答を得られない可能性があります。

編集:MattieuM が指摘したようにmutable、複数のスレッドは一緒にうまく動作しません。

ちょっと興味深いことはさておき、std::normal_distribution一度に 2 つの値を生成し、1 つをキャッシュします (したがって_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.

于 2013-04-15T06:40:27.107 に答える
1

他の答えは言う:

これはすべて理論上の話ですが、 const に限定されない理由は、実装者が必要に応じて実装を変更できるようにするためだと思います。さらに、より統一されたインターフェイスを維持します。一部の operator() が const であり、一部が非 const である場合、すべてが少し面倒になります。

これはおおむね正しいですが、ジェネリック プログラミングのコンテキストではさらに深いです。(@Calimoが言ったように、これは「念のためconst」省略された考えを残します)。

このことを考えた結果、次のメンバー関数が原則 constとして成り立つかどうかの問題は実際の の型に依存するという結論に達しました_UniformRandomNumberGenerator

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

(ジェネリック) 仕様のこのレベルでは、これは不明であるため、「[仕様] 実装者が [内部状態] を変更できるようにする」のはそのときだけであり、ジェネリック性のためにそれを行います。

したがって、constness の問題は、コンパイル時_UniformRandomNumberGeneratorに、分布がサンプル描画を生成するのに十分な乱数 (ビット) を生成できるかどうかを知る必要 があることです。

現在の仕様では、その可能性は除外されていますが、メンバー関数の 2 つの排他的なバージョンを使用することで、原則として実装 (または指定) できます。

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. ステートレスな方法で分散ロジックを自分で実装します。

(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倍遅くなります(公称状態をコピー/変更/破棄するため)が、あなたが指摘するように、それが重要な場合は、より「概念的な」コンテキストで使用できますデザイン(私は理解できます)。

(スレッド間で共有された不変のディストリビューションを使用できるという、別の小さな無関係な利点があります)


正しくないが例示的なコードは次のとおりです。

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

aこのステートレスな実装は非常に「効率的」ですが、 andの任意の値b(分布の限界)に対して 100% 正しいわけではありません。ご覧のとおり、他のディストリビューション (継続的なディストリビューションを含む) では、この方法は非常に難しく、扱いにくく、エラーが発生しやすいため、お勧めしません。


これは主に個人的な意見です。状況を改善できますか?

はい、ほんの少しだけです。

ディストリビューションには 2 つのバージョンがありますoperator()。1 つは no- const(つまり&) で、これは最適 (現在のもの) で、もう 1 つ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);
   }
};
于 2020-02-26T11:06:19.453 に答える