【问题标题】:How the new range-based for loop in C++17 helps Ranges TS?C++17 中新的基于范围的 for 循环如何帮助 Ranges TS?
【发布时间】:2017-05-03 20:27:01
【问题描述】:

委员会将基于范围的 for 循环从:

  • C++11:

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

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

人们说这将使 Ranges TS 的实现更加容易。你能给我一些例子吗?

【问题讨论】:

  • 我能看到的唯一区别是 1. 实现要求 __begin 和 __end 是同一类型。不需要第二次实施。
  • 是的。该提案本身在动机中指出:现有的基于范围的 for 循环受到过度约束。结束迭代器永远不会递增、递减或取消引用。要求它是一个迭代器没有任何实际目的。放宽基于范围的 for 循环的类型要求,可为 Ranges TS 的用户提供最佳体验。我想知道最好的体验是什么样的。 open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0184r0.html
  • 我想主要是为了支持代理端迭代器。
  • Ranges 允许哨兵作为结束标记(例如在以空结尾的字符串中),如果 __begin 和 __end 都是迭代器,则这是不可能的。

标签: c++ c++11 for-loop c++17


【解决方案1】:

新规范允许__begin__end 是不同的类型,只要__end 可以与__begin 进行比较以判断不等式。 __end 甚至不需要是迭代器,也可以是谓词。这是一个愚蠢的示例,其中定义了 beginend 成员的结构,后者是谓词而不是迭代器:

#include <iostream>
#include <string>

// a struct to get the first word of a string

struct FirstWord {
    std::string data;

    // declare a predicate to make ' ' a string ender

    struct EndOfString {
        bool operator()(std::string::iterator it) { return (*it) != '\0' && (*it) != ' '; }
    };

    std::string::iterator begin() { return data.begin(); }
    EndOfString end() { return EndOfString(); }
};

// declare the comparison operator

bool operator!=(std::string::iterator it, FirstWord::EndOfString p) { return p(it); }

// test

int main() {
    for (auto c : {"Hello World !!!"})
        std::cout << c;
    std::cout << std::endl; // print "Hello World !!!"

    for (auto c : FirstWord{"Hello World !!!"}) // works with gcc with C++17 enabled
        std::cout << c;
    std::cout << std::endl; // print "Hello"
}

【讨论】:

  • 是的。这是很好的例子,谢谢。但我试图找到 Ranges TS 的具体示例。
  • @DimitarMircev:范围 TS 实际上并没有定义任何范围。它定义了许多作用于范围的算法,以及允许人们编写使用范围的代码的概念 TS 概念。但是 Range TS v1 不提供任何实际的范围类型。所以没有可以提供的例子。
  • @NicolBolas 为什么它与 Ranges TS 相关?我认为这是因为 Ranges TS 支持这些非对称迭代器/哨兵范围。
  • @Yakk:是的,Range TS 定义了允许迭代器/哨兵配对的范围概念。但它没有定义使用它们的任何实际范围。因此,Range TS 唯一能展示的就是一个概念。这只是说“迭代器/哨兵配对没问题”,这是我们已经知道的。它没有显示他们使用的示例。
  • 我在n4128中举了一个例子。另请参阅appendix on sentinels and code generation
【解决方案2】:

C++11/14 range-for 被过度约束...

WG21 论文是P0184R0,其动机如下:

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

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

...这会扭曲 operator== 的定界迭代器。

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

#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 使用 g++ -std=c++14,(assembly 使用 gcc.godbolt.org)

上面的operator==StringIterator&lt;&gt; 的参数是对称的,并且不依赖于range-for 是begin != end 还是end != begin(否则你可以作弊并将代码切成两半)。

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

C++17 将放宽限制以简化分隔范围...

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

#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 使用 g++ -std=c++1z(assembly 使用 gcc.godbolt.org,几乎与前面的示例相同)。

...实际上将支持完全通用的原始“D 样式”范围。

WG21论文N4382有以下建议:

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

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

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

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;                }
};

如果不知道原始范围的底层表示,如何从中提取迭代器?如何使其适应可与 range-for 一起使用的范围?这是一种方法(另见@EricNiebler 的series of blog posts)和来自@T.C. 的cmets:

#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 使用 g++ -std=c++1z(assembly 使用 gcc.godbolt.org)

结论:哨兵不仅仅是一种将分隔符压入类型系统的可爱机制,它们对于support primitive "D-style" ranges(它们本身可能没有迭代器的概念)作为零开销来说足够通用新的 C++1z range-for 的抽象。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-01-04
    • 2016-10-31
    • 1970-01-01
    • 2021-04-08
    相关资源
    最近更新 更多