program story

C ++ 17의 새로운 범위 기반 for 루프가 Ranges TS에 어떻게 도움이 되나요?

inputbox 2020. 11. 21. 14:09
반응형

C ++ 17의 새로운 범위 기반 for 루프가 Ranges TS에 어떻게 도움이 되나요?


위원회는 범위 기반 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를 더 쉽게 구현할 수 있다고 말했습니다. 몇 가지 예를 들어 주시겠습니까?


C ++ 11 / 14 범위- for과도하게 제한되었습니다 ...

이에 대한 WG21 논문은 P0184R0 이며 다음과 같은 동기가 있습니다.

기존 범위 기반 for 루프는 과도하게 제한됩니다. 끝 반복자는 증가, 감소 또는 역 참조되지 않습니다. 반복자가되도록 요구하는 것은 실용적인 목적이 아닙니다.

게시 한 Standardese에서 알 수 있듯이 end범위 반복자는 loop-condition에서만 사용됩니다 __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;
}

g ++ -std = c ++ 14를 사용한 라이브 예제 , (gcc.godbolt.org를 사용한 어셈블리 )

위의 operator==for StringIterator<>는 인수가 대칭 적이며 범위 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;
}

g ++ -std = c ++ 1z를 사용한 라이브 예제 (이전 예제와 거의 동일한 gcc.godbolt.org를 사용한 어셈블리 ).

...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.


The new specification allows __begin and __end to be of different type, as long as __end can be compared to __begin for inequality. __end doesn't even need to be an iterator and can be a predicate. Here is a silly example with a struct defining begin and end members, the latter being a predicate instead of an iterator:

#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"
}

참고URL : https://stackoverflow.com/questions/39117330/how-the-new-range-based-for-loop-in-c17-helps-ranges-ts

반응형