std::shuffle 不能使用 std::list 编译

std::shuffle doesn't compile with std::list

本文关键字:std list 编译 不能 shuffle      更新时间:2023-10-16

我正在尝试洗牌一些生成的元素列表。这是代码:

std::default_random_engine generator (10);
std::list<int> list(10);
int n = 0;
std::generate(list.begin(), list.end(), [&]{ return n++; });
std::shuffle(list.begin(), list.end(), generator);

它不编译。以下是错误:

/include/c++/v1/algorithm:3059:34: Invalid operands to binary expression ('std::__1::__list_iterator<int, void *>' and 'std::__1::__list_iterator<int, void *>')
main.cpp:1:10: In file included from main.cpp:1:
/include/c++/v1/random:1641:10: In file included from /bin/../include/c++/v1/random:1641:
main.cpp:37:10: In instantiation of function template specialization 'std::__1::shuffle<std::__1::__list_iterator<int, void *>, std::__1::linear_congruential_engine<unsigned int, 48271, 0, 2147483647> &>' requested here
/include/c++/v1/iterator:622:1: Candidate template ignored: could not match 'reverse_iterator' against '__list_iterator'
/include/c++/v1/iterator:1017:1: Candidate template ignored: could not match 'move_iterator' against '__list_iterator'
/include/c++/v1/iterator:1369:1: Candidate template ignored: could not match '__wrap_iter' against '__list_iterator'
/include/c++/v1/string:486:11: Candidate template ignored: could not match 'fpos' against '__list_iterator'

有人知道吗?

std::list不提供

对其元素的随机访问,这是std::shuffle()需要的。这是std::shuffle()在其规范中签名的样子(C++标准第25.3.12段):

template<class RandomAccessIterator, class UniformRandomNumberGenerator>
void shuffle(RandomAccessIterator first,
             RandomAccessIterator last,
             UniformRandomNumberGenerator&& g);

如果可以,请考虑改用std::vector - 顺便说一下,C++ 标准本身鼓励您将其用作默认顺序容器。

举个例子(科利鲁的现场演示):

int main()
{
    std::default_random_engine generator(10);
    std::vector<int> v(10);
    std::iota(begin(v), end(v), 0);
    std::shuffle(begin(v), end(v), generator);
    for (auto x : v) { std::cout << x; }
}

std::iota()算法只是您特定用法的更简单的替代方案 std::generate .

std::shuffle需要随机访问迭代器。 std::list不提供这些。您需要不同的容器,例如 std::vector

如果你真的需要std::list,你可能需要在专用算法中实现洗牌。但首先确保你真的需要它。很多时候,人们认为他们需要std::list,而他们真的需要std::vector

[algorithms.general]/2, shuffle声明:

template<class RandomAccessIterator, class UniformRandomNumberGenerator>
void shuffle(RandomAccessIterator first, RandomAccessIterator last,
              UniformRandomNumberGenerator&& rand);

[..]

如果算法的模板参数RandomAccessIterator [..] 实际的模板参数应满足随机访问迭代器 (24.2.7) 的要求。

显然,std::list只提供双向迭代器。尝试改用提供随机访问迭代器的容器。

AndyProwl正确地解释了为什么代码无法编译,并指出使用std::vector通常比使用std::list更合适。然而,juanchopanza在他的主张中略微感到害怕,即洗牌一个std::list需要一个专门的算法。事实上,使用 std::shuffle 很容易打乱 std::list 的中间向量(如果适用,请使用 move 构造函数):

#include <numeric>
#include <random>
template <typename T, typename URBG>
void shuffle(std::list<T>& l, URBG&& urbg)
{
    std::vector<std::reference_wrapper<const T>> v(l.begin(), l.end());
    std::shuffle(v.begin(), v.end(), urbg);
    std::list<T> shuffled;
    for (auto &ref : v) shuffled.push_back(std::move(ref.get()));
    l.swap(shuffled);
}
int main()
{
    std::list<int> l(10);
    std::iota(l.begin(), l.end(), 0);
    shuffle(l, std::mt19937{ std::random_device{}() });
    for (auto x : l) std::cout << x << " ";
}

事实上,我们可以做得更好:由于可以在列表之间移动元素而不会使迭代器或引用失效,我们甚至可以打乱既不可复制也不可移动的对象列表,并且(也许更重要的是)还可以确保洗牌保留对列表元素的引用。

template <typename T, typename URBG>
void shuffle(std::list<T>& l, URBG&& urbg)
{
    std::vector<std::list<T>::const_iterator> v;
    for (auto it = l.cbegin(); it != l.cend(); ++it) v.push_back(it);
    std::shuffle(v.begin(), v.end(), urbg);
    std::list<T> shuffled;
    for (auto &it : v) shuffled.splice(shuffled.end(), l, it);
    l.swap(shuffled);
}

当然,这两种解决方案对于引用(或迭代器)向量都有 O(n) 空间开销。如果这是一个问题,那么您可以实现一个仅使用 O(1) 空间的较慢的 O(n log n) 时间算法,如此处所述。尽管无论如何,您可能会通过切换到std::forward_list来节省更多空间。