在同时为 C++03 和 C++14 的代码中进行简单随机洗牌的最佳实践是什么?

What are best practices for simple random shuffling in code that's both C++03 and C++14?

本文关键字:随机 简单 最佳 是什么 C++03 代码 C++14      更新时间:2023-10-16

背景:我正在为一个简单的游戏洗牌矢量的元素。通过传递相同的整数种子,应该可以再次玩相同的游戏 - 反之亦然,不同的种子应该产生不同的游戏。加密安全性(或任何严格性)不是设计目标;代码的清洁度是一个设计目标。

C++98/C++03 引入了std::random_shuffle,其用法如下:

int seed = ...;
std::srand(seed);  // caveat, see below
std::vector<int> deck = ...;
std::random_shuffle(deck.begin(), deck.end());

但是,截至 C++14,random_shuffle已被弃用(来源:N3924)。洗牌的C++14方法是

int seed = ...;
std::vector<int> deck = ...;
std::shuffle(deck.begin(), deck.end(), std::mt19937(seed));

以下是每种方法的减损因素:

  • srand/random_shuffle 方式在 C++14 中已弃用,因此我们不应该使用它。

  • 在某些实现中,random_shuffle似乎不会从srand中获取种子,即具有不同值的种子不会产生不同的输出!(Linux 上的 libstdc++ 没有这个问题,但 OSX 10.9.5 上的 Xcode 有。

  • shuffle/mt19937方式不是 C++03 的一部分,所以我们不能使用它。

  • shuffle/mt19937方式似乎要求我们将种子一直传递到甲板洗牌代码中。对于我的应用程序,我宁愿通过隐藏全局变量的机制(例如srand)"设置它并忘记它",而不必定义我自己的类型 mt19937 的全局 PRNG。 换句话说:我不想被PRNG细节所困扰,我只想洗牌我的向量!

  • 我有点担心线程安全性(能够同时从不同的线程
  • 洗牌两个不同的甲板),但显然不是同时担心线程安全性和"可播种性"。将线程安全视为"可有可无"。

我想到的第一个候选人是:

  • 咬紧牙关,int seed一直传递到甲板洗牌代码中(避免全局变量)

  • 使用类似 #if __cplusplus >= 20110000 的内容来使用 C++11 之前的random_shuffle和 C++11 之后的shuffle

  • 要解决 OSX 上srand"错误",请使用带有一些复杂函子的三参数版本的 random_shuffle......这听起来很丑

第二个候选者是:

  • 螺丝 C++03;只需放弃对任何不提供std::shuffle的支持,开箱即用std::mt19937

但是有没有解决这个问题的好方法?我知道这是一个没有人有的问题,除非他们的程序是玩具程序;但是一定有数百个玩具程序遇到了这个问题!

首先,开发一个所需的接口。它应该对用户隐藏任何平台/编译器细节,但为您提供实现所需的所有数据。编写具有所需用法的单元测试。像这样:

int seed = ...;
std::vector<int> deck = ...;
my_shuffle(deck.begin(), deck.end(), seed);

然后实施

template< typename IteratorType >
void my_shuffle( IteratorType first, IteratorType last, 
                                       int seed = some_default_seed_maybe )
{
     #ifdef MACOS_FOUND
         // Mac solution
     #elif __cplusplus >= 201103L
         // C++11 solution
     #else
         // fallback
}

它看起来足够干净吗?

另请检查: 如何在 C 预处理器中可靠地检测 Mac OS X、iOS、Linux、Windows?

即使在 C++11 中,发行版在实现中也没有标准化。

编写自己的洗牌器(对于每个元素,将其与另一个随机序列交换)和随机数生成器/分布。 弱、慢的随机数生成器简短而简单。

我会将您的"随机工厂"向下传递,并且是一种在线程生成时"分叉"的方法,因为这样做还可以让您在同一次执行中执行多次"运行"。 使用显式状态而不是全局状态通常是值得的。 但如果是单线程,则不需要:只需将随机工厂塞入某个全局状态并捏住鼻子即可。

没有随机的洗牌可以桥接C++03到C++17,所以要拥抱一个简短的手写。 它还确保在多个平台上具有相同的行为,这很有用,原因有很多(测试覆盖率(在不同平台上相同),跨平台测试(OS/X上的错误可以在Windows上调试),无数东西的可移植性(保存游戏文件,基于io的网络游戏玩法等))。

boost::random::mt19937作为后备吗?我为线程执行此操作,几乎没有担心。

我可能会想做这样的事情。它通过将种子包装在自定义类中来解决"传递种子"问题。为了减少工作量,我私下从std::vector<int>继承了这些功能,并刚刚实现了我实际上需要deck的功能。

私有继承通过确保我无法将deck*分配给其基指针(从而避免非虚拟析构函数问题)为我提供了一些保护。

#if __cplusplus >= 201103L
class deck
: private std::vector<int>
{
    int seed = 0;
public:
    deck(int seed = 0): seed(seed) {}
    // access relevant functions
    using std::vector<int>::size;
    using std::vector<int>::begin;
    using std::vector<int>::end;
    using std::vector<int>::push_back;
    using std::vector<int>::operator=;
    using std::vector<int>::operator[];
    void shuffle()
    {
        std::shuffle(begin(), end(), std::mt19937(seed));
    }
};
#else
class deck
: private std::vector<int>
{
    typedef std::vector<int> vector;
    typedef vector::iterator iterator;
    int seed = 0;
public:
    deck(int seed = 0): seed(seed) {}
    // implement relevant functions
    iterator begin() { return vector::begin(); }
    iterator end() { return vector::end(); }
    void push_back(int i) { vector::push_back(i); }
    int& operator[](vector::size_type i) { return (*this)[i]; }
    void shuffle()
    {
        std::srand(seed);
        std::random_shuffle(begin(), end());
    }
};
#endif
int main()
{
    deck d(5);
    d = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    d.shuffle();
    for(unsigned i = 0; i < d.size(); ++i)
        std::cout << d[i] << 'n';
}