如何为 C++20 范围实现一个只调用生成器函数的惰性“范围工厂”?

jwe*_*rek 8 c++ c++20 std-ranges

我喜欢你可以使用惰性范围的想法std::views::iota,但惊讶地发现这iota是目前标准中唯一类似的东西;views::single它是除了和之外唯一的“范围工厂” views::empty。例如,目前还没有相当于std::generate炼油厂的工厂。

然而我注意到,通过在 iota 上使用变换视图来实现语义是微不足道的generate,并且忽略 iota 传递给变换的值,即

#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';
}
Run Code Online (Sandbox Code Playgroud)

我的问题是实施这样的事情的“真正方法”是什么?制作一个可通过管道传输的范围视图对象,而不仅仅是使用iota.

我尝试继承ranges::view_interface- 不知道这是否是正确的方法 - 并且只是让它返回一个调用生成器函数的虚拟迭代器,但我的代码不起作用,因为它需要将范围视图通过管道传递到std::views::takein为了不导致无限循环。我在这里定义的对象最终不会成为可管道化的。

#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';
}
Run Code Online (Sandbox Code Playgroud)

康桓瑋*_*康桓瑋 7

无法工作的原因generate2是它没有对range概念建模,即它返回的类型begin()没有建模input_iterator,因为input_iterator需要difference_typevalue_type存在并且i++是一个有效的表达式。

另外,你的迭代器不满足sentinel_for<iterator>,这意味着它不能作为自己的哨兵,因为sentinel_for需要semiregular它需要default_initializable,所以你还需要为它添加默认构造函数。

您还需要重写bool operator!=(...)sincebool operator==(...) constoperator!=反向合成operator==。但在您的情况下用作default_sentinel_t哨兵会更容易。

如果你添加它们,iterator你会发现代码格式良好

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;
  }
  // ...
};
Run Code Online (Sandbox Code Playgroud)

但是,operator*()ofiterator不满足equal-preserving的要求,也就是说,前后两次调用得到的结果不相等,这意味着这将是未定义的行为。

可以参考实现使用成员变量来缓存每次生成的结果,那么每次调用ranges::istream_view时只需要返回缓存的值即可。iterator::operator*()

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_; 
};
Run Code Online (Sandbox Code Playgroud)

  • 事实上,对于`r | 的形式 std::views::xxx`,gcc和msvc都只是检查表达式的有效性,所以错误信息对于用户来说并不直观。我通常将其转换为`std::ranges::xxx_view(r)`形式进行调试,这样可以相对清楚地告诉我不满足哪个特定约束。使用概念来提供更有意义的错误消息是一个不断发展的愿景,但现实是有时概念错误消息并没有多大帮助。 (2认同)