c++ 是否可以/建议返回一个范围?

o2g1uqev  于 2024-01-09  发布在  其他
关注(0)|答案(7)|浏览(160)

我正在使用范围库来帮助在我的类中过滤数据,像这样:

  1. class MyClass
  2. {
  3. public:
  4. MyClass(std::vector<int> v) : vec(v) {}
  5. std::vector<int> getEvens() const
  6. {
  7. auto evens = vec | ranges::views::filter([](int i) { return ! (i % 2); });
  8. return std::vector<int>(evens.begin(), evens.end());
  9. }
  10. private:
  11. std::vector<int> vec;
  12. };

字符串
在本例中,getEvents()函数中构造了一个新的vector。为了保存这个开销,我想知道是否可以/建议直接从函数返回range?

  1. class MyClass
  2. {
  3. public:
  4. using RangeReturnType = ???;
  5. MyClass(std::vector<int> v) : vec(v) {}
  6. RangeReturnType getEvens() const
  7. {
  8. auto evens = vec | ranges::views::filter([](int i) { return ! (i % 2); });
  9. // ...
  10. return evens;
  11. }
  12. private:
  13. std::vector<int> vec;
  14. };


如果可能的话,我是否需要考虑一些终身的因素?
我还想知道是否可以/建议将范围作为参数传入,或者将其存储为成员变量。或者范围库更适合在单个函数的作用域内使用?

dfddblmv

dfddblmv1#

在c++23中,可以使用std::generatorco_yieldstd::ranges::elements_of

  1. class MyClass
  2. {
  3. public:
  4. MyClass(std::vector<int> v) : vec(v) {}
  5. std::generator<int> getEvens() const
  6. {
  7. auto evens = vec | std::ranges::views::filter([](int i) { return ! (i % 2); });
  8. co_yield std::ranges::elements_of(evens);
  9. }
  10. private:
  11. std::vector<int> vec;
  12. };
  13. int main() {
  14. MyClass mc{{1,2,3,4,5,6,7,8,9}};
  15. for (int i : mc.getEvens()) {
  16. std::cout << i << '\n';
  17. }
  18. }

字符串
工作演示(GCC 13.1不带std::ranges::elements_of):https://godbolt.org/z/oehd59oEz

展开查看全部
tv6aics1

tv6aics12#

这是在OP的评论部分被问到的,但我想我会在回答部分回答它:
Ranges图书馆似乎很有希望,但我对这辆返回的汽车有点担心。
请记住,即使添加了auto,C++也是一种强类型语言。在您的情况下,由于您返回evens,因此返回类型将与evens相同。(技术上它将是evens的值类型,但evens无论如何都是值类型)
实际上,您可能真的不想手动输入返回类型:std::ranges::filter_view<std::ranges::ref_view<const std::vector<int>>, MyClass::getEvens() const::<decltype([](int i) {return ! (i % 2);})>>(141个字符)

  • 正如@Caleth在评论中提到的,事实上,这也不起作用,因为evens是在函数内部定义的lambda,两个不同的lambda类型即使基本相同也会不同,所以这里实际上没有办法获得完整的返回类型。

虽然在不同的情况下是否使用auto可能会有争论,但我相信大多数人都会在这里使用auto。再加上你的evens也是用auto声明的,输入类型只会让这里的可读性降低。
那么,如果我想访问一个子集(例如偶数),我有什么选择呢?有没有其他方法我应该考虑,有或没有范围库?
根据您如何访问返回的数据以及数据的类型,您可能会考虑返回std::vector<T*>
views实际上应该从头到尾查看。虽然您可以使用views::dropviews::take限制为单个元素,但它没有提供下标操作符(尚未)。
也会有计算上的差异。vector需要预先计算,其中views是在迭代时计算的。所以当你这样做时:

  1. for(auto i : myObject.getEven())
  2. {
  3. std::cout << i;
  4. }

字符串
在引擎盖下,它基本上是这样做的:

  1. for(auto i : myObject.vec)
  2. {
  3. if(!(i % 2)) std::cout << i;
  4. }


根据数据量和计算的复杂性,views可能会快得多,或者与vector方法差不多。另外,您可以轻松地在同一范围内应用多个过滤器,而无需多次迭代数据。
最后,您可以始终将view存储在vector中:

  1. std::vector<int> vec2(evens.begin(), evens.end());


所以我的建议是,如果你有范围库,那么你应该**使用它。
如果不是,则vector<T>vector<T*>vector<index>取决于T的大小和可复制性。

展开查看全部
bqucvtff

bqucvtff3#

正如你所看到的here,一个范围就是你可以调用beginend的东西,仅此而已。
例如,您可以使用begin(range)的结果(它是一个迭代器)来遍历range,使用++运算符来推进它。
一般来说,回顾我上面链接的概念,只要conext代码只需要能够调用beginend,就可以使用范围。
显然,如果你的意图是将evens传递给一个需要std::vector的函数,(例如,它是一个你不能改变的函数,它在我们正在谈论的实体上调用.push_back),你显然必须从filter的输出中生成std::vector,我会通过

  1. auto evens = vec | ranges::views::filter(whatever) | ranges::to_vector;

字符串
但是如果你传递给evens的所有函数都是在它上面循环,那么

  1. return vec | ranges::views::filter(whatever);


就可以了
关于生命周期的考虑,视图指向一个值的范围,就像指针指向被指向的实体一样:如果后者被销毁,前者将被悬挂,并且不正确地使用它将是未定义的行为。这是一个错误的程序:

  1. #include <iostream>
  2. #include <range/v3/view/filter.hpp>
  3. #include <string>
  4. using namespace ranges;
  5. using namespace ranges::views;
  6. auto f() {
  7. // a local vector here
  8. std::vector<std::string> vec{"zero","one","two","three","four","five"};
  9. // return a view on the local vecotor
  10. return vec | filter([](auto){ return true; });
  11. } // vec is gone ---> the view returned is dangling
  12. int main()
  13. {
  14. // the following throws std::bad_alloc for me
  15. for (auto i : f()) {
  16. std::cout << i << std::endl;
  17. }
  18. }

展开查看全部
ia2d9nvy

ia2d9nvy4#

标准中对STL的组件的使用没有限制。当然,有最佳实践(例如,string_view而不是string const &)。
在这种情况下,我可以预见直接处理视图返回类型没有问题。也就是说,最佳实践还没有决定,因为标准是如此之新,还没有编译器有一个完整的实现。
我认为,你可以选择以下几点:

  1. class MyClass
  2. {
  3. public:
  4. MyClass(std::vector<int> v) : vec(std::move(v)) {}
  5. auto getEvens() const
  6. {
  7. return vec | ranges::views::filter([](int i) { return ! (i % 2); });
  8. }
  9. private:
  10. std::vector<int> vec;
  11. };

字符串

展开查看全部
biswetbf

biswetbf5#

您可以将ranges::any_view用作任何范围或范围组合的类型擦除机制。

  1. ranges::any_view<int> getEvens() const
  2. {
  3. return vec | ranges::views::filter([](int i) { return ! (i % 2); });
  4. }

字符串
我在STL范围库中看不到任何等效的;如果可以的话,请编辑答案。
编辑:ranges::any_view的问题是它非常慢和低效。参见https://github.com/ericniebler/range-v3/issues/714

tyg4sfes

tyg4sfes6#

我不会对RangesV 3发表评论,所以ranges::any_view不是一个选项。只在标准库范围C++20版本上发言。
所以我遇到了一个类似的问题,当我试图使用视图来返回一个所选元素的非所有视图,并能够使用它与一个常规向量相同的接口,例如,不必双重解引用(指向值的指针或迭代器的向量。)下面是尝试在tokenizer方法中使用视图的结果,该方法可以消除空格/换行符标记。REDUNDANT_TOKEN_KINDS是将被过滤的令牌类型的枚举的初始化器列表。例如TokenType::whitespace注意:rvector等效于下面代码中的std::vector。

  1. auto sanitize(const rvector<Token>& tokens) {
  2. rvector<Tokenizer::token_iterator> sanitized;
  3. for (auto it = tokens.cbegin(); it != tokens.cend(); ++it) {
  4. if (std::any_of(REDUNDANT_TOKEN_KINDS.begin(),
  5. REDUNDANT_TOKEN_KINDS.end(),
  6. [it](TokenKind match) { return match == it->type(); })) {
  7. continue;
  8. };
  9. sanitized.push_back(it);
  10. }
  11. auto sanitized_view = sanitized | std::views::transform(
  12. [&tokens](token_iterator iter) -> const Token & {
  13. return *iter;});
  14. // Create a non-owning view of iterators using std::ranges::subrange
  15. return sanitized_view;
  16. }

字符串
现在,这很好,但是返回类型是什么呢?如果我想创建另一个方法,比如parse_tokens(token_view_type token_view),我不能这样做,因为我不能确定token视图的返回类型。
我可以这样做(使用自动参数类型):

  1. void parse_tokens(const auto & token_view)


但这是非常丑陋和不安全的代码,这意味着我的代码库的其余部分将不得不依赖于一个单一的方法sanitize(),它的未知返回类型。我将不得不传递auto,直到我将范围的元素复制到向量中。我可以指定范围的概念::参数中的范围。这更好。但这仍然给我们留下了sanitize()返回类型的问题这对程序员来说并不明显。此外,这将创建parse_tokens方法的无关模板示例。

  1. auto parse_tokens(const std::ranges::range auto & token_view)


所以一般来说,我会说不建议返回一个范围或视图,因为这意味着所有后续代码都必须适应这种模式。就像你说的,在封装的环境(方法或方法集合)中进行所有算法处理,然后将所需结果作为迭代器或引用的向量返回会更聪明。
我的解决方案是返回一个包含“selected”或“filtered”值的std::reference_wrappers的vector。代码如下:

  1. // Returns a non-modifiable vector of const references to the subrange of tokens which are not redundant.
  2. static const token_view sanitize(const rvector<Token>& tokens) {
  3. rvector<std::reference_wrapper<const Token>> sanitized;
  4. for (auto it = tokens.cbegin(); it != tokens.cend(); ++it) {
  5. if (std::any_of(REDUNDANT_TOKEN_KINDS.begin(),
  6. REDUNDANT_TOKEN_KINDS.end(),
  7. [it](TokenKind match) { return match == it->type(); })) {
  8. continue;
  9. };
  10. sanitized.push_back(*it);
  11. }
  12. return sanitized;
  13. }


这有一个小缺点,即在迭代时不能直接返回,你必须调用它->get()来获取底层引用对象。

  1. // Inside Parser class....
  2. // Omitted code....
  3. // token_view_iterator is of type :
  4. // rvector<std::reference_wrapper<const Token>>::const_iterator
  5. const Token & get(token_view_iterator cursor) {
  6. if (cursor >= tokens_end()) {
  7. return Token{ TokenKind::eof };
  8. }
  9. // Must do cursor->get() , cannot simply dereference *cursor
  10. return cursor->get();
  11. }


总的来说,函数是相同的,并且使代码更加清晰(不是很漂亮吗?)所有的指针逻辑都在std::reference_wrapper的引擎盖下处理,所以你不会弄乱指针。

展开查看全部
7z5jn7bk

7z5jn7bk7#

最好在头文件中声明一个返回范围的函数,并在cpp文件中定义它
1.用于编译防火墙(编译速度)
1.阻止语言服务器发疯
1.为了更好地分解代码,
然而,有一些并发症使其不可取:How to get type of a view?

  • 如果在头文件中定义它很好,则使用auto
  • 如果性能不是问题,我推荐ranges::any_view
  • 否则我会说这是不可取的。

相关问题