您将如何为仅调用生成器函数的 C++20 范围实现惰性 "range factory"?
How would you implement a lazy "range factory" for C++20 ranges that just calls a generator function?
我喜欢可以使用 std::views::iota
实现的惰性范围的想法,但很惊讶地发现 iota
目前是标准中唯一类似的东西;它是除views::single
和views::empty
之外唯一的“范围工厂”。例如,目前没有 std::generate
作为范围工厂的等价物。
我注意到,通过在 iota 上使用转换视图并忽略 iota 传递给转换的值来实现 generate
的语义是微不足道的,即
#include <iostream>
#include <ranges>
#include <random>
template<typename F>
auto generate1(const F& func) {
return std::views::iota(0) | std::views::transform([&func](int) {return func(); });
}
std::random_device dev;
std::mt19937 rng(dev());
int main() {
auto d6 = []() {
static std::uniform_int_distribution<> dist(1, 6);
return dist(rng);
};
for (int v : generate1(d6) | std::views::take(10)) {
std::cout << v << ' ';
}
std::cout << '\n';
}
我的问题是什么是实现这样的东西的“真正方法”?要制作可管道化的范围视图对象,而不仅仅是使用 iota
.
我尝试从 ranges::view_interface
继承——不知道这是否是正确的方法——只是让它 return 一个调用生成器函数的虚拟迭代器,但我的代码不起作用因为它需要将范围视图通过管道传输到 std::views::take
的部分,以免导致无限循环。我在此处定义的对象最终无法通过管道传输。
#include <iostream>
#include <ranges>
#include <random>
template<typename F>
class generate2 : public std::ranges::view_interface<generate2<F>>
{
using value_type = decltype(std::declval<F>()());
class iterator {
const F* gen_func_;
public:
iterator(const F* f) : gen_func_(f)
{}
value_type operator*() const {
return (*gen_func_)();
}
bool operator!=(const iterator&) {
return true;
}
iterator& operator++() {
return *this;
}
};
F generator_func_;
public:
generate2(const F& f) : generator_func_(f) {
}
iterator begin() {
return iterator(&generator_func_);
}
iterator end() {
return iterator(nullptr);
}
};
std::random_device dev;
std::mt19937 rng(dev());
int main() {
auto d6 = []() {
static std::uniform_int_distribution<> dist(1, 6);
return dist(rng);
};
// the following doesnt compile because of the pipe...
for (int v : generate2(d6) | std::views::take(10)) {
std::cout << v << ' ';
}
std::cout << '\n';
}
之所以generate2
不能工作是因为它没有对range
概念进行建模,即其begin()
所编辑的类型return不进行建模input_iterator
,因为 input_iterator
需要 difference_type
和 value_type
存在并且 i++
是一个有效的表达式。
另外,你的迭代器不满足sentinel_for<iterator>
,也就是说不能作为自己的sentinel,因为sentinel_for
需要semiregular
,而semiregular
需要default_initializable
,所以你还需要为它添加默认构造函数。
您还需要将 bool operator!=(...)
重写为 bool operator==(...) const
,因为 operator!=
不会反向合成 operator==
。但是在你的情况下使用 default_sentinel_t
作为哨兵更容易。
如果将它们添加到 iterator
,您会发现代码将是 well-formed:
class iterator {
public:
using value_type = decltype(std::declval<F>()());
using difference_type = std::ptrdiff_t;
iterator() = default;
void operator++(int);
bool operator==(const iterator&) const {
return false;
}
// ...
};
但是iterator
的operator*()
不满足equality-preserving的要求,也就是说得到的结果前后两次调用不相等,这意味着这将是未定义的行为。
可以参考ranges::istream_view
的实现,使用一个成员变量来缓存每次生成的结果,那么每次调用iterator::operator*()
时只需要return缓存的值.
template<typename F>
class generate2 : public std::ranges::view_interface<generate2<F>> {
public:
auto begin() {
value_ = generator_func_();
return iterator{*this};
}
std::default_sentinel_t end() const noexcept { return std::default_sentinel; }
class iterator {
public:
//...
value_type operator*() const {
return parent_->value_;
}
private:
generate2* parent_;
};
private:
F generator_func_;
std::remove_cvref_t<std::invoke_result_t<F&>> value_;
};
我喜欢可以使用 std::views::iota
实现的惰性范围的想法,但很惊讶地发现 iota
目前是标准中唯一类似的东西;它是除views::single
和views::empty
之外唯一的“范围工厂”。例如,目前没有 std::generate
作为范围工厂的等价物。
我注意到,通过在 iota 上使用转换视图并忽略 iota 传递给转换的值来实现 generate
的语义是微不足道的,即
#include <iostream>
#include <ranges>
#include <random>
template<typename F>
auto generate1(const F& func) {
return std::views::iota(0) | std::views::transform([&func](int) {return func(); });
}
std::random_device dev;
std::mt19937 rng(dev());
int main() {
auto d6 = []() {
static std::uniform_int_distribution<> dist(1, 6);
return dist(rng);
};
for (int v : generate1(d6) | std::views::take(10)) {
std::cout << v << ' ';
}
std::cout << '\n';
}
我的问题是什么是实现这样的东西的“真正方法”?要制作可管道化的范围视图对象,而不仅仅是使用 iota
.
我尝试从 ranges::view_interface
继承——不知道这是否是正确的方法——只是让它 return 一个调用生成器函数的虚拟迭代器,但我的代码不起作用因为它需要将范围视图通过管道传输到 std::views::take
的部分,以免导致无限循环。我在此处定义的对象最终无法通过管道传输。
#include <iostream>
#include <ranges>
#include <random>
template<typename F>
class generate2 : public std::ranges::view_interface<generate2<F>>
{
using value_type = decltype(std::declval<F>()());
class iterator {
const F* gen_func_;
public:
iterator(const F* f) : gen_func_(f)
{}
value_type operator*() const {
return (*gen_func_)();
}
bool operator!=(const iterator&) {
return true;
}
iterator& operator++() {
return *this;
}
};
F generator_func_;
public:
generate2(const F& f) : generator_func_(f) {
}
iterator begin() {
return iterator(&generator_func_);
}
iterator end() {
return iterator(nullptr);
}
};
std::random_device dev;
std::mt19937 rng(dev());
int main() {
auto d6 = []() {
static std::uniform_int_distribution<> dist(1, 6);
return dist(rng);
};
// the following doesnt compile because of the pipe...
for (int v : generate2(d6) | std::views::take(10)) {
std::cout << v << ' ';
}
std::cout << '\n';
}
之所以generate2
不能工作是因为它没有对range
概念进行建模,即其begin()
所编辑的类型return不进行建模input_iterator
,因为 input_iterator
需要 difference_type
和 value_type
存在并且 i++
是一个有效的表达式。
另外,你的迭代器不满足sentinel_for<iterator>
,也就是说不能作为自己的sentinel,因为sentinel_for
需要semiregular
,而semiregular
需要default_initializable
,所以你还需要为它添加默认构造函数。
您还需要将 bool operator!=(...)
重写为 bool operator==(...) const
,因为 operator!=
不会反向合成 operator==
。但是在你的情况下使用 default_sentinel_t
作为哨兵更容易。
如果将它们添加到 iterator
,您会发现代码将是 well-formed:
class iterator {
public:
using value_type = decltype(std::declval<F>()());
using difference_type = std::ptrdiff_t;
iterator() = default;
void operator++(int);
bool operator==(const iterator&) const {
return false;
}
// ...
};
但是iterator
的operator*()
不满足equality-preserving的要求,也就是说得到的结果前后两次调用不相等,这意味着这将是未定义的行为。
可以参考ranges::istream_view
的实现,使用一个成员变量来缓存每次生成的结果,那么每次调用iterator::operator*()
时只需要return缓存的值.
template<typename F>
class generate2 : public std::ranges::view_interface<generate2<F>> {
public:
auto begin() {
value_ = generator_func_();
return iterator{*this};
}
std::default_sentinel_t end() const noexcept { return std::default_sentinel; }
class iterator {
public:
//...
value_type operator*() const {
return parent_->value_;
}
private:
generate2* parent_;
};
private:
F generator_func_;
std::remove_cvref_t<std::invoke_result_t<F&>> value_;
};