C++17 中新的基于范围的 for 循环如何帮助 Ranges TS? [英] How the new range-based for loop in C++17 helps Ranges TS?

查看:42
本文介绍了C++17 中新的基于范围的 for 循环如何帮助 Ranges TS?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

委员会改变了基于范围的 for 循环:

  • C++11:

    <代码>{汽车&&__range = range_expression ;for (auto __begin = begin_expr, __end = end_expr;__开始!= __结束;++__开始){range_declaration = *__begin;循环语句}}

  • 到 C++17:

    <代码>{汽车&&__range = range_expression ;自动 __begin = begin_expr ;自动 __end = end_expr ;for ( ; __begin != __end; ++__begin) {range_declaration = *__begin;循环语句}}

人们说这将使实施 Ranges TS 更容易.你能给我举一些例子吗?

解决方案

C++11/14 range-for was overconstrained...

WG21 论文是 P0184R0 其动机如下:

<块引用>

现有的基于范围的 for 循环受到过度约束.结束迭代器永远不会递增、递减或取消引用.需要它作为一个迭代器没有实际用途.

从您发布的 Standardese 中可以看出,范围的 end 迭代器仅用于循环条件 __begin != __end;.因此,end 只需要与 begin 相等,不需要可解引用或可递增.

...它扭曲了分隔迭代器的 operator==.

那么这有什么缺点呢?好吧,如果您有一个标记分隔的范围(C 字符串、文本行等),那么您必须将循环条件硬塞到迭代器的 operator== 中,本质上是这样

#include 模板 结构体字符串迭代器{char const* ptr = nullptr;朋友自动运算符==(StringIterator lhs,StringIterator rhs){返回 lhs.ptr ?(rhs.ptr || (*lhs.ptr == Delim)) : (!rhs.ptr || (*rhs.ptr == Delim));}朋友自动运算符!=(StringIterator lhs,StringIterator rhs){返回 !(lhs == rhs);}自动&运算符*() { 返回 *ptr;}自动&运算符++(){++ptr;返回 *this;}};模板 类 StringRange{StringIterator它;上市:StringRange(char const* ptr) : it{ptr} {}自动开始(){ 返回它;}auto end() { return StringIterator{};}};int main(){//Hello World",没有感叹号for (auto const& c : StringRange<'!'>{"Hello World!"})std::cout <<C;}

实时示例 使用 g++ -std=c++14,(组装使用 gcc.godbolt.org)

上述 StringIterator<>operator== 在其参数上是对称的,并且不依赖于 range-for 是否为 begin != endend != begin (否则你可以作弊并将代码切成两半).

对于简单的迭代模式,编译器能够优化operator==内部的复杂逻辑.事实上,对于上面的例子,operator== 被简化为一个单一的比较.但这会继续适用于范围和过滤器的长管道吗?谁知道.很可能需要英雄般的优化级别.

C++17 将放宽约束,这将简化分隔范围...

那么简化究竟体现在什么地方呢?在 operator== 中,现在有额外的重载采用迭代器/哨兵对(在两个顺序中,为了对称).所以运行时逻辑变成了编译时逻辑.

#include 模板 struct StringSentinel {};结构体字符串迭代器{char const* ptr = nullptr;模板 <char Delim>朋友自动操作符==(StringIterator lhs, StringSentinel rhs) {返回 *lhs.ptr == Delim;}模板 <char Delim>朋友自动操作符==(StringSentinel lhs, StringIterator rhs) {返回rhs == lhs;}模板 <char Delim>朋友自动运算符!=(StringIterator lhs,StringSentinel rhs){返回 !(lhs == rhs);}模板 <char Delim>朋友自动运算符!=(StringSentinel lhs, StringIterator rhs){返回 !(lhs == rhs);}自动&运算符*() { 返回 *ptr;}自动&运算符++(){++ptr;返回 *this;}};模板 类 StringRange{StringIterator 吧;上市:StringRange(char const* ptr) : it{ptr} {}自动开始(){ 返回它;}auto end() { return StringSentinel{};}};int main(){//Hello World",没有感叹号for (auto const& c : StringRange<'!'>{"Hello World!"})std::cout <<C;}

现场示例 使用 g++ -std=c++1z(组装,使用 gcc.godbolt.org,几乎与上一个例子).

...实际上将支持完全通用的原始D 风格"范围.

WG21 论文 N4382 有以下建议:

<块引用>

C.6 Range Facade 和适配器实用程序 [future.facade]

1 直到它用户创建自己的迭代器类型变得微不足道,完整的迭代器的潜力将仍未实现.范围抽象使之成为可能.使用正确的库组件,它应该是用户可以用最少的界面定义一个范围(例如,currentdonenext 成员),并具有迭代器类型自动生成.这样的范围外观类模板保留为未来的工作.

本质上,这等于 D 风格的范围(这些原语被称为 emptyfrontpopFront).只有这些原语的分隔字符串范围看起来像这样:

template 类 PrimitiveStringRange{字符常量* ptr;上市:PrimitiveStringRange(char const* c) : ptr{c} {}自动&当前(){ 返回 *ptr;}auto done() const { return *ptr == Delim;}自动下一个(){++ptr;}};

如果不知道原始范围的底层表示,如何从中提取迭代器?如何将其调整为可与 range-for 一起使用的范围?这是一种方法(另请参阅@EricNiebler 的系列博文) 和@TC 的评论:

#include //在迭代器/哨兵对的开始/结束对当前/完成/旁边调整任何原始范围模板<派生类>struct RangeAdaptor : 私有派生{使用派生::派生;结构哨兵{};结构体迭代器{派生* rng;朋友自动操作符==(Iterator it, Sentinel) { return it.rng->done();}朋友自动运算符==(Sentinel,Iterator it){ return it.rng->done();}朋友自动运算符!=(迭代器lhs,哨兵rhs){返回!(lhs==rhs);}朋友自动运算符!=(哨兵lhs,迭代器rhs){返回!(lhs==rhs);}自动&运算符*(){返回rng->当前();}自动&运算符++(){rng->next();返回 *this;}};自动开始(){返回迭代器{this};}自动结束(){返回哨兵{};}};int main(){//Hello World",没有感叹号for (auto const& c : RangeAdaptor<PrimitiveStringRange<'!'>>{"Hello World!"})std::cout <<C;}

现场示例 使用 g++ -std=c++1z(组装使用 gcc.godbolt.org)

结论:哨兵不仅是一种将分隔符压入类型系统的可爱机制,它们还足够通用支持原始的D 风格"范围(它们本身可能没有迭代器的概念)作为新的 C++1z range-for 的零开销抽象.>

The committee changed the range-based for loop from:

  • C++11:

    {
       auto && __range = range_expression ; 
       for (auto __begin = begin_expr, __end = end_expr; 
           __begin != __end; ++__begin) { 
           range_declaration = *__begin; 
           loop_statement 
       }
    } 
    

  • to C++17 :

    {        
        auto && __range = range_expression ; 
        auto __begin = begin_expr ;
        auto __end = end_expr ;
        for ( ; __begin != __end; ++__begin) { 
            range_declaration = *__begin; 
            loop_statement 
        } 
    }
    

And people said that this will make implementing Ranges TS easier. Can you give me some examples?

解决方案

C++11/14 range-for was overconstrained...

The WG21 paper for this is P0184R0 which has the following motivation:

The existing range-based for loop is over-constrained. The end iterator is never incremented, decremented, or dereferenced. Requiring it to be an iterator serves no practical purpose.

As you can see from the Standardese that you posted, the end iterator of a range is only used in the loop-condition __begin != __end;. Hence end only needs to be equality comparable to begin, and it does not need to be dereferenceable or incrementable.

...which distorts operator== for delimited iterators.

So what disadvantage does this have? Well, if you have a sentinel-delimited range (C-string, line of text, etc.), then you have to shoehorn the loop-condition into the iterator's operator==, essentially like this

#include <iostream>

template <char Delim = 0>
struct StringIterator
{
    char const* ptr = nullptr;   

    friend auto operator==(StringIterator lhs, StringIterator rhs) {
        return lhs.ptr ? (rhs.ptr || (*lhs.ptr == Delim)) : (!rhs.ptr || (*rhs.ptr == Delim));
    }

    friend auto operator!=(StringIterator lhs, StringIterator rhs) {
        return !(lhs == rhs);
    }

    auto& operator*()  {        return *ptr;  }
    auto& operator++() { ++ptr; return *this; }
};

template <char Delim = 0>
class StringRange
{
    StringIterator<Delim> it;
public:
    StringRange(char const* ptr) : it{ptr} {}
    auto begin() { return it;                      }
    auto end()   { return StringIterator<Delim>{}; }
};

int main()
{
    // "Hello World", no exclamation mark
    for (auto const& c : StringRange<'!'>{"Hello World!"})
        std::cout << c;
}

Live Example with g++ -std=c++14, (assembly using gcc.godbolt.org)

The above operator== for StringIterator<> is symmetric in its arguments and does not rely on whether the range-for is begin != end or end != begin (otherwise you could cheat and cut the code in half).

For simple iteration patterns, the compiler is able to optimize the convoluted logic inside operator==. Indeed, for the above example, the operator== is reduced to a single comparison. But will this continue to work for long pipelines of ranges and filters? Who knows. It is likely to require heroic optimization levels.

C++17 will relax the constraints which will simplify delimited ranges...

So where exactly does the simplification manifest itself? In operator==, which now has extra overloads taking an iterator/sentinel pair (in both orders, for symmetry). So the run time logic becomes compile time logic.

#include <iostream>

template <char Delim = 0>
struct StringSentinel {};

struct StringIterator
{
    char const* ptr = nullptr;   

    template <char Delim>
    friend auto operator==(StringIterator lhs, StringSentinel<Delim> rhs) {
        return *lhs.ptr == Delim;
    }

    template <char Delim>
    friend auto operator==(StringSentinel<Delim> lhs, StringIterator rhs) {
        return rhs == lhs;
    }

    template <char Delim>
    friend auto operator!=(StringIterator lhs, StringSentinel<Delim> rhs) {
        return !(lhs == rhs);
    }

    template <char Delim>
    friend auto operator!=(StringSentinel<Delim> lhs, StringIterator rhs) {
        return !(lhs == rhs);
    }

    auto& operator*()  {        return *ptr;  }
    auto& operator++() { ++ptr; return *this; }
};

template <char Delim = 0>
class StringRange
{
    StringIterator it;
public:
    StringRange(char const* ptr) : it{ptr} {}
    auto begin() { return it;                      }
    auto end()   { return StringSentinel<Delim>{}; }
};

int main()
{
    // "Hello World", no exclamation mark
    for (auto const& c : StringRange<'!'>{"Hello World!"})
        std::cout << c;
}

Live Example using g++ -std=c++1z (assembly using gcc.godbolt.org, which is almost identical to the previous example).

...and will in fact support fully general, primitive "D-style" ranges.

WG21 paper N4382 has the following suggestion:

C.6 Range Facade and Adaptor Utilities [future.facade]

1 Until it becomes trivial for users to create their own iterator types, the full potential of iterators will remain unrealized. The range abstraction makes that achievable. With the right library components, it should be possible for users to define a range with a minimal interface (e.g., current, done, and next members), and have iterator types automatically generated. Such a range facade class template is left as future work.

Essentially, this is equal to D-style ranges (where these primitives are called empty, front and popFront). A delimited string range with only these primitives would look something like this:

template <char Delim = 0>
class PrimitiveStringRange
{
    char const* ptr;
public:    
    PrimitiveStringRange(char const* c) : ptr{c} {}
    auto& current()    { return *ptr;          }
    auto  done() const { return *ptr == Delim; }
    auto  next()       { ++ptr;                }
};

If one does not know the underlying representation of a primitive range, how to extract iterators from it? How to adapt this to a range that can be used with range-for? Here's one way (see also the series of blog posts by @EricNiebler) and the comments from @T.C.:

#include <iostream>

// adapt any primitive range with current/done/next to Iterator/Sentinel pair with begin/end
template <class Derived>
struct RangeAdaptor : private Derived
{      
    using Derived::Derived;

    struct Sentinel {};

    struct Iterator
    {
        Derived*  rng;

        friend auto operator==(Iterator it, Sentinel) { return it.rng->done(); }
        friend auto operator==(Sentinel, Iterator it) { return it.rng->done(); }

        friend auto operator!=(Iterator lhs, Sentinel rhs) { return !(lhs == rhs); }
        friend auto operator!=(Sentinel lhs, Iterator rhs) { return !(lhs == rhs); }

        auto& operator*()  {              return rng->current(); }
        auto& operator++() { rng->next(); return *this;          }
    };

    auto begin() { return Iterator{this}; }
    auto end()   { return Sentinel{};     }
};

int main()
{
    // "Hello World", no exclamation mark
    for (auto const& c : RangeAdaptor<PrimitiveStringRange<'!'>>{"Hello World!"})
        std::cout << c;
}

Live Example using g++ -std=c++1z (assembly using gcc.godbolt.org)

Conclusion: sentinels are not just a cute mechanism to press delimiters into the type system, they are general enough to support primitive "D-style" ranges (which themselves may have no notion of iterators) as a zero-overhead abstraction for the new C++1z range-for.

这篇关于C++17 中新的基于范围的 for 循环如何帮助 Ranges TS?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆