Thread: Iterator quandary

  1. #1
    Registered User Sir Galahad's Avatar
    Join Date
    Nov 2016
    Location
    The Round Table
    Posts
    277

    Iterator quandary

    I am trying to implement a reasonable iterator interface, but the STL has me a little confused. Let's say we have defined the following (somewhat nonsensical) function:

    Code:
    #include <cstdlib>
    
    template <typename ForwardIterator>
    ForwardIterator write_char(int ch, std::size_t count, ForwardIterator next, ForwardIterator eof) {
      while (count--) {
        if (next == eof)
          return ForwardIterator();
        *next++ = ch;
      }
      return next;
    }

    The iteration policy seems pretty reasonable to me. Compare with operator ==, dereference once, and return the updated position. In the case where iterator "hits the wall", return a default-initialized instance of the iterator type.

    Then we invoke it like this:

    Code:
    #include <iostream>
    
    using namespace std;
    
    int main() {
      char buf[5] = {0};
      char* end = buf + sizeof(buf);
      char* pos = write_char('1', 3, buf, end);
      cout << "buf: '" << buf << "' (" << (pos - buf) << " chars written)" << endl;
      char* uho = write_char('!', 3, pos, end);
      if (uho == NULL)
        cout << "Error: buffer is full!" << endl;
    }
    So far, so good...or so I thought. Let's try it with "std::back_inserter":

    Code:
    #include <iostream>
    #include <string>
    #include <iterator>
    
    using namespace std;
    
    int main() {
      string buf;
      auto pos = write_char('?', 3, back_inserter(buf), back_inserter());
    }
    Well that doesn't compile. There is no default constructor for "back_inserter" and you can't even compare them with each other either. I am obviously using the wrong STL object here. But what to use? Surely there is a way to put this all together into a generic interface?
    Last edited by Sir Galahad; 08-13-2023 at 08:58 AM.

  2. #2
    Registered User
    Join Date
    Dec 2017
    Posts
    1,633
    back_inserter() is syntactic sugar to automatically deduce the type for a back_insert_iterator (although post C++17 back_insert_iterator can deduce it's own type). Doing so requires a parameter. There is no default initialization for back_insert_iterator. At any rate, it's hardly an iterator at all. Both * and ++ are no-ops. It just calls push_back on the container when used with =. There is no real eof for such an operation (besides throwing an exception if size() == max_size()).

    Maybe add another function:
    Code:
    template <typename C>
    auto write_char(int ch, std::size_t count, std::back_insert_iterator<C> next) {
      while (count--) next = ch;
      return next;
    }
    I assume you are including cstdlib for std::size_t. The minimal include for this would be cstddef.
    Also, remember to use nullptr instead of NULL.
    And you can just use \n instead of endl most of the time (endl adds a flush but interactive output is usually line-buffered anyway).
    A little inaccuracy saves tons of explanation. - H.H. Munro

  3. #3
    Registered User Sir Galahad's Avatar
    Join Date
    Nov 2016
    Location
    The Round Table
    Posts
    277
    Quote Originally Posted by john.c View Post
    back_inserter() is syntactic sugar to automatically deduce the type for a back_insert_iterator (although post C++17 back_insert_iterator can deduce it's own type). Doing so requires a parameter. There is no default initialization for back_insert_iterator. At any rate, it's hardly an iterator at all. Both * and ++ are no-ops. It just calls push_back on the container when used with =. There is no real eof for such an operation (besides throwing an exception if size() == max_size()).

    Maybe add another function:
    Code:
    template <typename C>
    auto write_char(int ch, std::size_t count, std::back_insert_iterator<C> next) {
      while (count--) next = ch;
      return next;
    }
    I assume you are including cstdlib for std::size_t. The minimal include for this would be cstddef.
    Also, remember to use nullptr instead of NULL.
    And you can just use \n instead of endl most of the time (endl adds a flush but interactive output is usually line-buffered anyway).
    Only problem with that is it seems to violate the "separation of concerns" principal. The code I am working is meant to be used as a library, so I would really rather not design it specifically around any particular STL iterator. What I really want is a generic interface that "plays nice" with the standard library, while not necessarily bending to all of its idiosyncrasies.

    I suppose I could provide an adapter class that converts a back_insert_iterator to a pair of iterators that could be passed to the routine but that too could turn out to be just as much of a kludge, in and of itself.

    What would be really nice would be a single-object interface (as opposed to the "iterator pair" paradigm). I have yet to come up with a bug-free example of such a thing, so I will refrain from elaborating unless and until I do. Just an interesting thought anyway....

  4. #4
    Registered User
    Join Date
    Dec 2017
    Posts
    1,633
    I agree it's a kludge. I couldn't think of a good way of doing it. I'm definitely not an expert at C++ and laserlight seems to be gone. You should ask on cplusplus.com where they have one or two actual experts. (If you do, let us know the answer.)

    What would be really nice would be a single-object interface (as opposed to the "iterator pair" paradigm).
    I can't imagine how you would do your first example without an end pointer.
    A little inaccuracy saves tons of explanation. - H.H. Munro

  5. #5
    Registered User Sir Galahad's Avatar
    Join Date
    Nov 2016
    Location
    The Round Table
    Posts
    277
    Quote Originally Posted by john.c View Post
    I agree it's a kludge. I couldn't think of a good way of doing it. I'm definitely not an expert at C++ and laserlight seems to be gone. You should ask on cplusplus.com where they have one or two actual experts. (If you do, let us know the answer.)


    I can't imagine how you would do your first example without an end pointer.
    Thanks for the pointers. This is what I was referring to. I'll start by defining a class with explicit methods just to demonstrate the basic idea.

    Code:
    template <typename Container>
    class fixed_iterant_object_t {
      typedef typename Container::iterator iter_t;
    
      iter_t current, end;
    
     public:
      fixed_iterant_object_t(Container& reference)
          : current(reference.begin()), end(reference.end()) {}
    
      fixed_iterant_object_t() : current(iter_t()), end(iter_t()) {}
    
      fixed_iterant_object_t& next() {
        if (current == end)
          throw;
        ++current;
        return *this;
      }
    
      typename Container::value_type& data() {
        if (current == end)
          throw;
        return *current;
      }
    
      bool good() { return current != end; }
    };
    
    template <typename Container>
    fixed_iterant_object_t<Container> fixed_iterant_object(Container& reference) {
      return fixed_iterant_object_t<Container>(reference);
    }
    
    #include <iostream>
    #include <string>
    
    using namespace std;
    
    int main() {
      string str = "Hello iterant object!";
      auto fio = fixed_iterant_object(str);
      while (fio.good()) {
        cout << fio.data();
        fio.next();
      }
      cout << '\n';
    }
    So the three methods (along with the default constructor to indicate a 'nil' object) lay down the core interface for these so-called "iterant" objects.

    (And yes, in this toy example I am simply throwing without a proper exception on error.)

    I did try to extend the idea to something more like a classic iterator, but ran into issues with precedence (order of evaluation):

    Code:
    template <typename Container>
    struct fixed_iterant_t {
      typedef typename Container::iterator iter_t;
    
      iter_t current, end;
    
      fixed_iterant_t(Container& reference)
          : current(reference.begin()), end(reference.end()) {}
    
      fixed_iterant_t() : current(iter_t()), end(iter_t()) {}
    
      fixed_iterant_t& operator++() {
        if (current == end)
          throw;
        ++current;
        return *this;
      }
    
      fixed_iterant_t operator++(int) {
        fixed_iterant_t tmp = *this;
        ++(*this);
        return tmp;
      }
    
      typename Container::value_type& operator*() {
        if (current == end)
          throw;
        return *current;
      }
    
      operator bool() { return current != end; }
    };
    
    template <typename Container>
    fixed_iterant_t<Container> fixed_iterant(Container& reference) {
      return fixed_iterant_t<Container>(reference);
    }
    
    template <typename Container>
    class push_back_iterant_t {
      typedef typename Container::value_type value_t;
      Container* pointer;
      value_t current;
    
     public:
      push_back_iterant_t(Container& reference)
          : pointer(&reference), current(value_t()) {}
    
      push_back_iterant_t() : pointer(nullptr), current(value_t()) {}
    
      push_back_iterant_t& operator++() {
        if (pointer == nullptr)
          throw;
        pointer->push_back(current);
        current = value_t();
        return *this;
      }
    
      push_back_iterant_t operator++(int) {
        push_back_iterant_t tmp = *this;
        ++(*this);
        return tmp;
      }
    
      value_t& operator*() {
        if (pointer == nullptr)
          throw;
        return current;
      }
    
      operator bool() { return pointer != nullptr; }
    };
    
    template <typename Container>
    push_back_iterant_t<Container> push_back_iterant(Container& reference) {
      return push_back_iterant_t<Container>(reference);
    }
    
    #include <iostream>
    #include <vector>
    
    using namespace std;
    
    int main() {
      vector<char> buf;
      auto pbi = push_back_iterant(buf);
      string str = "Hello iterant!";
      auto fit = fixed_iterant(str);
      while (fit)
        *pbi++ = *fit++;
      *pbi++ = 0;
      cout << "str: " << str << '\n';
      cout << "buf: " << &buf[0] << '\n';
    }
    The problem seems to be that "*iter++" kinds of expressions are NOT evaluated as a pointer would be. Which is to say instead of "dereference, then increment" we get "increment, then dereference". From what I can tell there is really no way around the issue. Hopefully not?
    Last edited by Sir Galahad; 08-15-2023 at 01:34 PM.

  6. #6
    Registered User
    Join Date
    Dec 2017
    Posts
    1,633
    There's nothing wrong with the precedence. current starts with '\0'. the post-increment pushes it to the container then sets it to '\0' again and returns A COPY OF THE OLD ITERATOR, which has its current set to the given char by * (and the default =). The "real" current is still '\0', so you get a bunch of zeroes in the container.

    And it's not really right for the increment operator to modify the container. I think the ++'s and * should be no-ops and you need to implement operator=, just like the back_inserter.

    A back_insert_iterator is a kind of output iterator, which is what you are trying to implement. They are kind of strange: C++ named requirements: LegacyOutputIterator - cppreference.com
    A little inaccuracy saves tons of explanation. - H.H. Munro

  7. #7
    Registered User Sir Galahad's Avatar
    Join Date
    Nov 2016
    Location
    The Round Table
    Posts
    277
    Quote Originally Posted by john.c View Post
    There's nothing wrong with the precedence. current starts with '\0'. the post-increment pushes it to the container then sets it to '\0' again and returns A COPY OF THE OLD ITERATOR, which has its current set to the given char by * (and the default =). The "real" current is still '\0', so you get a bunch of zeroes in the container.
    Ok, I finally get what you are saying now. The dereference happens on the copy, not the original object. But that was also (kind of!) my point. With a pointer, an expression like "*ptr++" is evaluated by FIRST dereferencing the pointer, THEN incrementing it. Whereas in the case of objects which overload these operators, the process is more along the lines of: (1) post-increment original object (2) create temporary (3) pre-increment original object, and finally (4) dereference copy. So the net effect is that the order of operations are in a sense reversed.

    Quote Originally Posted by john.c View Post

    And it's not really right for the increment operator to modify the container. I think the ++'s and * should be no-ops and you need to implement operator=, just like the back_inserter.
    The whole point is to create a UNIFIED interface for both input and output. Maybe a proxy class would be in order here to ensure that classes such as "push_back_iterant" behave properly? (Remember, the other one worked just fine with those semantics because it doesn't have the same sort of complex internal state.)

    Quote Originally Posted by john.c View Post
    A back_insert_iterator is a kind of output iterator, which is what you are trying to implement. They are kind of strange: C++ named requirements: LegacyOutputIterator - cppreference.com
    Though not really trying to implement an STL-style output iterator here. My original complaint, in fact, is that I just don't like the interface. For one thing, there is no concept of "checking for validity" with that paradigm. It's just insert-or-fail, basically. I would prefer a more flexible interface. That said, the check could naturally be made a no-op for certain classes.

  8. #8
    Registered User Sir Galahad's Avatar
    Join Date
    Nov 2016
    Location
    The Round Table
    Posts
    277
    I think I've found an even better approach! We can "have our cake and eat it too" if we simply toss out the assumption that an iterant could ever even be in "equilibrium". Instead, the primary rule becomes: any access with respect to internal state implies an iteration. So whether getting or setting a value, the iterant shall always "move forward".

    The result is fairly elegant (save for the fact that most output iterants will likely need to return a proxy object in order for everything to work properly). Verbosity is reduced an almost absolute minimum!

    Code:
    template <typename Container>
    class fixed_iterant_t {
      typedef typename Container::iterator iter_t;
      typedef typename Container::value_type value_t;
    
      iter_t current, end;
    
     public:
      fixed_iterant_t(Container& reference)
          : current(reference.begin()), end(reference.end()) {}
    
      fixed_iterant_t() : current(iter_t()), end(iter_t()) {}
    
      value_t& operator*() {
        if (current == end)
          throw;
        value_t& val = *current;
        ++current;
        return val;
      }
    
      inline operator bool() { return current != end; }
    };
    
    template <typename Container>
    fixed_iterant_t<Container> fixed_iterant(Container& reference) {
      return fixed_iterant_t<Container>(reference);
    }
    
    template <typename Container>
    class push_back_iterant_t {
      Container* pointer;
    
     public:
      push_back_iterant_t(Container& reference) : pointer(&reference) {}
    
      push_back_iterant_t() : pointer(nullptr) {}
    
      struct proxy {
        push_back_iterant_t& reference;
    
        proxy(push_back_iterant_t* owner) : reference(*owner) {}
    
        proxy& operator=(const typename Container::value_type& value) {
          reference.pointer->push_back(value);
          return *this;
        }
      };
    
      inline proxy operator*() {
        if (pointer == nullptr)
          throw;
        return proxy(this);
      }
    
      inline operator bool() { return pointer != nullptr; }
    };
    
    template <typename Container>
    push_back_iterant_t<Container> push_back_iterant(Container& reference) {
      return push_back_iterant_t<Container>(reference);
    }
    
    #include <iostream>
    #include <vector>
    
    using namespace std;
    
    int main() {
      vector<char> buf;
      auto pbi = push_back_iterant(buf);
      string str = "Hello Iterant!";
      auto fit = fixed_iterant(str);
      while (fit)
        *pbi = *fit;
      *pbi = 0;
      cout << "str: " << str << '\n';
      cout << "buf: " << &buf[0] << '\n';
    }
    Last edited by Sir Galahad; 08-17-2023 at 04:44 AM. Reason: proxy& operator= should take a const reference and push_back_iterant_t no longer requires internal value

  9. #9
    Registered User Sir Galahad's Avatar
    Join Date
    Nov 2016
    Location
    The Round Table
    Posts
    277
    Another fun example:

    Code:
    template <typename Container>
    class fixed_reverse_iterant_t {
      typedef typename Container::reverse_iterator iter_t;
      typedef typename Container::value_type value_t;
    
      iter_t current, end;
    
     public:
      fixed_reverse_iterant_t(Container& reference)
          : current(reference.rbegin()), end(reference.rend()) {}
    
      fixed_reverse_iterant_t() : current(iter_t()), end(iter_t()) {}
    
      value_t& operator*() {
        if (current == end)
          throw;
        value_t& ref = *current;
        ++current;
        return ref;
      }
    
      inline operator bool() { return current != end; }
    };
    
    template <typename Container>
    fixed_reverse_iterant_t<Container> fixed_reverse_iterant(Container& reference) {
      return fixed_reverse_iterant_t<Container>(reference);
    }
    
    #include <iostream>
    #include <string>
    
    using namespace std;
    
    int main() {
      string fwd = "Ever seen an iterant run backwards?";
      cout << fwd << '\n';
      auto rev = fixed_reverse_iterant(fwd);
      while (rev)
        cout << *rev;
      cout << '\n';
    }

  10. #10
    Registered User
    Join Date
    Dec 2017
    Posts
    1,633
    With a pointer, an expression like "*ptr++" is evaluated by FIRST dereferencing the pointer, THEN incrementing it.
    No, it's the other way around. To do it the way you are saying you would need to say (*ptr)++. The precedence doesn't change with operator overloads.
    Postfix inc is above prefix inc. Prefix inc is same level as dereference: C++ Operator Precedence - cppreference.com

    (I haven't looked at your new code yet and may not be able to until later today. But I will definitely look at it.)
    A little inaccuracy saves tons of explanation. - H.H. Munro

  11. #11
    Registered User Sir Galahad's Avatar
    Join Date
    Nov 2016
    Location
    The Round Table
    Posts
    277
    Quote Originally Posted by john.c View Post
    No, it's the other way around. To do it the way you are saying you would need to say (*ptr)++. The precedence doesn't change with operator overloads.
    Postfix inc is above prefix inc. Prefix inc is same level as dereference: C++ Operator Precedence - cppreference.com

    (I haven't looked at your new code yet and may not be able to until later today. But I will definitely look at it.)
    Ah, right! I was definitely using the wrong terminology there. What I am talking about is the fact that *object++ behaves differently from *pointer++. If you print debugging statements from a class that overloads operators * and postfix ++, you will find that the latter function is called first.

    For example, consider the following:

    Code:
    #include <iostream>
    
    using namespace std;
    
    class broken_example {
     public:
      void operator*() { cout << "operator*" << endl; }
    
      broken_example& operator++(int) {
        cout << "operator++" << endl;
        return *this;
      }
    };
    
    int main() {
      *broken_example()++;
    }
    Output:

    operator++
    operator*
    So essentially, we get the opposite of how an ordinary pointer behaves.

  12. #12
    Registered User
    Join Date
    Dec 2017
    Posts
    1,633
    What I am talking about is the fact that *object++ behaves differently from *pointer++. ... So essentially, we get the opposite of how an ordinary pointer behaves.
    That's not correct. They are exactly the same. The postfix++ is called first, then the *, for both.
    The postfix++ returns the old value, before the increment, of course, but it's still called first in both cases.
    Maybe you're getting confused because the * is applied to the old value, but it's not applied until after the increment (at least conceptually; there could be an optimization to do it first so a copy doesn't need to be made in the pure pointer case).

    Here's an example with assembly, compiled without optimization:
    Code:
    #include <stdio.h>
     
    int main() {
        int a[] = {0,1,2,3,4};
        int *p = a;
        int n = *p++;
        int m = *p++;
        printf("%d %d\n", n, m);
        return 0;
    }
    Code:
    .LC0:
        .string    "%d %d\n"
        .text
        .globl    main
        .type    main, @function
    main:
    .LFB0:
        .cfi_startproc
        pushq    %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        subq    $48, %rsp
        movq    %fs:40, %rax
        movq    %rax, -8(%rbp)
        xorl    %eax, %eax
        movl    $0, -32(%rbp)
        movl    $1, -28(%rbp)
        movl    $2, -24(%rbp)
        movl    $3, -20(%rbp)
        movl    $4, -16(%rbp)
     
        ; p = a
        leaq    -32(%rbp), %rax
        movq    %rax, -40(%rbp)
     
        ; p++
        movq    -40(%rbp), %rax
        leaq    4(%rax), %rdx
        movq    %rdx, -40(%rbp)   ; stores new value back to stack
     
        ; n = *p
        movl    (%rax), %eax      ; uses old value, still in rax
        movl    %eax, -48(%rbp)
     
        ; p++
        movq    -40(%rbp), %rax
        leaq    4(%rax), %rdx
        movq    %rdx, -40(%rbp)
     
        ; m = *p
        movl    (%rax), %eax
        movl    %eax, -44(%rbp)
     
        ; printf("%d %d\n", n, m)
        movl    -44(%rbp), %edx
        movl    -48(%rbp), %eax
        movl    %eax, %esi
        leaq    .LC0(%rip), %rdi
        movl    $0, %eax
        call    printf@PLT
    I had a chance to look through you code. I feel like you're doing what I suggested but in a more complicated way. Instead of having the proxy, just define operator= in the push_back_iterant_t class. Define prefix++, postfix++, and * as no-ops. That's basically what the back_insert_iterator does.

    I don't see the point of the nullptr part. When will the pointer ever be null?
    BTW, all methods defined within a class declaration are automatically inline.
    Code:
    template <typename Container>
    class fixed_iterant_t {
      typedef typename Container::iterator iter_t;
      typedef typename Container::value_type value_t;
     
      iter_t current, end;
     
     public:
      fixed_iterant_t(Container& reference)
          : current(reference.begin()), end(reference.end()) {}
     
      fixed_iterant_t() : current(iter_t()), end(iter_t()) {}
     
      fixed_iterant_t& operator++() {
        if (current == end) throw;
        ++current;
        return *this;
      }
     
      fixed_iterant_t operator++(int) {
        auto tmp = *this;
        ++(*this);
        return tmp;
      }
     
      value_t& operator*() {
        if (current == end) throw;
        return *current;
      }
     
      operator bool() { return current != end; }
    };
     
    template <typename Container>
    fixed_iterant_t<Container> fixed_iterant(Container& reference) {
      return fixed_iterant_t<Container>(reference);
    }
     
    template <typename Container>
    class push_back_iterant_t {
      Container* pointer;
     public:
      push_back_iterant_t(Container& reference) : pointer(&reference) {}
      push_back_iterant_t& operator++()    { return *this; }
      push_back_iterant_t  operator++(int) { return *this; }
      push_back_iterant_t& operator*()     { return *this; }
      auto operator=(const typename Container::value_type& value) {
        pointer->push_back(value);
        return value;
      }
    };
     
    template <typename Container>
    push_back_iterant_t<Container> push_back_iterant(Container& reference) {
      return push_back_iterant_t<Container>(reference);
    }
     
    #include <iostream>
    #include <vector>
    using namespace std;
     
    int main() {
      string buf;
      auto pbi = push_back_iterant(buf);
      string str = "Hello Iterant!";
      auto fit = fixed_iterant(str);
      while (fit) *pbi++ = *fit++;
      *pbi = 0;
      cout << "str: " << str << '\n';
      cout << "buf: " << buf.data() << '\n';
    }
    Last edited by john.c; 08-17-2023 at 06:53 PM.
    A little inaccuracy saves tons of explanation. - H.H. Munro

  13. #13
    Registered User Sir Galahad's Avatar
    Join Date
    Nov 2016
    Location
    The Round Table
    Posts
    277
    Quote Originally Posted by john.c View Post
    That's not correct. They are exactly the same. The postfix++ is called first, then the *, for both.
    The postfix++ returns the old value, before the increment, of course, but it's still called first in both cases.
    Maybe you're getting confused because the * is applied to the old value, but it's not applied until after the increment (at least conceptually; there could be an optimization to do it first so a copy doesn't need to be made in the pure pointer case).

    Here's an example with assembly, compiled without optimization:
    Code:
    #include <stdio.h>
     
    int main() {
        int a[] = {0,1,2,3,4};
        int *p = a;
        int n = *p++;
        int m = *p++;
        printf("%d %d\n", n, m);
        return 0;
    }
    Code:
    .LC0:
        .string    "%d %d\n"
        .text
        .globl    main
        .type    main, @function
    main:
    .LFB0:
        .cfi_startproc
        pushq    %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        subq    $48, %rsp
        movq    %fs:40, %rax
        movq    %rax, -8(%rbp)
        xorl    %eax, %eax
        movl    $0, -32(%rbp)
        movl    $1, -28(%rbp)
        movl    $2, -24(%rbp)
        movl    $3, -20(%rbp)
        movl    $4, -16(%rbp)
     
        ; p = a
        leaq    -32(%rbp), %rax
        movq    %rax, -40(%rbp)
     
        ; p++
        movq    -40(%rbp), %rax
        leaq    4(%rax), %rdx
        movq    %rdx, -40(%rbp)   ; stores new value back to stack
     
        ; n = *p
        movl    (%rax), %eax      ; uses old value, still in rax
        movl    %eax, -48(%rbp)
     
        ; p++
        movq    -40(%rbp), %rax
        leaq    4(%rax), %rdx
        movq    %rdx, -40(%rbp)
     
        ; m = *p
        movl    (%rax), %eax
        movl    %eax, -44(%rbp)
     
        ; printf("%d %d\n", n, m)
        movl    -44(%rbp), %edx
        movl    -48(%rbp), %eax
        movl    %eax, %esi
        leaq    .LC0(%rip), %rdi
        movl    $0, %eax
        call    printf@PLT
    Well sure, at the assembly level perhaps. But the expression "int n = *p++;" effectively dereferences the pointer before incrementing. In other words, it acts like "int n = *p; ++p;". On the other hand, with a class you have to explicitly account for the implications of the temporary.

    But yes, on the finer points you are 100% correct.


    Quote Originally Posted by john.c View Post
    I had a chance to look through you code. I feel like you're doing what I suggested but in a more complicated way. Instead of having the proxy, just define operator= in the push_back_iterant_t class. Define prefix++, postfix++, and * as no-ops. That's basically what the back_insert_iterator does.

    Code:
    template <typename Container>
    class fixed_iterant_t {
      typedef typename Container::iterator iter_t;
      typedef typename Container::value_type value_t;
     
      iter_t current, end;
     
     public:
      fixed_iterant_t(Container& reference)
          : current(reference.begin()), end(reference.end()) {}
     
      fixed_iterant_t() : current(iter_t()), end(iter_t()) {}
     
      fixed_iterant_t& operator++() {
        if (current == end) throw;
        ++current;
        return *this;
      }
     
      fixed_iterant_t operator++(int) {
        auto tmp = *this;
        ++(*this);
        return tmp;
      }
     
      value_t& operator*() {
        if (current == end) throw;
        return *current;
      }
     
      operator bool() { return current != end; }
    };
     
    template <typename Container>
    fixed_iterant_t<Container> fixed_iterant(Container& reference) {
      return fixed_iterant_t<Container>(reference);
    }
     
    template <typename Container>
    class push_back_iterant_t {
      Container* pointer;
     public:
      push_back_iterant_t(Container& reference) : pointer(&reference) {}
      push_back_iterant_t& operator++()    { return *this; }
      push_back_iterant_t  operator++(int) { return *this; }
      push_back_iterant_t& operator*()     { return *this; }
      auto operator=(const typename Container::value_type& value) {
        pointer->push_back(value);
        return value;
      }
    };
     
    template <typename Container>
    push_back_iterant_t<Container> push_back_iterant(Container& reference) {
      return push_back_iterant_t<Container>(reference);
    }
     
    #include <iostream>
    #include <vector>
    using namespace std;
     
    int main() {
      string buf;
      auto pbi = push_back_iterant(buf);
      string str = "Hello Iterant!";
      auto fit = fixed_iterant(str);
      while (fit) *pbi++ = *fit++;
      *pbi = 0;
      cout << "str: " << str << '\n';
      cout << "buf: " << buf.data() << '\n';
    }
    That makes sense. It also eliminates the temporary proxy object, so it's more efficient too.

    After thinking on it for a while, I think I am just going to go with an evern simpler approach. Rather than have iterant objects "act like a pointer", have them use a functor interface.

    Code:
    template <typename Container>
    class fixed_iterant_functor_t {
      typedef typename Container::iterator iter_t;
      typedef typename Container::value_type value_t;
    
      iter_t current, end;
    
     public:
      fixed_iterant_functor_t(Container& reference)
          : current(reference.begin()), end(reference.end()) {}
    
      fixed_iterant_functor_t() : current(iter_t()), end(iter_t()) {}
    
      value_t& operator()() {
        if (current == end)
          throw;
        value_t& val = *current;
        ++current;
        return val;
      }
    
      operator bool() { return current != end; }
    };
    
    template <typename Container>
    fixed_iterant_functor_t<Container> fixed_iterant_functor(Container& reference) {
      return fixed_iterant_functor_t<Container>(reference);
    }
    
    template <typename Container>
    class push_back_iterant_functor_t {
      Container* pointer;
    
     public:
      push_back_iterant_functor_t(Container& reference) : pointer(&reference) {}
    
      push_back_iterant_functor_t() : pointer(nullptr) {}
    
      push_back_iterant_functor_t& operator()(
          const typename Container::value_type& value) {
        if (pointer == nullptr)
          throw;
        pointer->push_back(value);
        return *this;
      }
    
      operator bool() { return pointer != nullptr; }
    };
    
    template <typename Container>
    push_back_iterant_functor_t<Container> push_back_iterant_functor(
        Container& reference) {
      return push_back_iterant_functor_t<Container>(reference);
    }
    
    #include <iostream>
    #include <vector>
    
    using namespace std;
    
    int main() {
      string buf;
      auto pbi = push_back_iterant_functor(buf);
      string str = "Hello Iterant Functor!";
      auto fit = fixed_iterant_functor(str);
      while (fit)
        pbi(fit());
      pbi(0);
      cout << "str: " << str << '\n';
      cout << "buf: " << buf.data() << '\n';
    }

    Quote Originally Posted by john.c View Post
    I don't see the point of the nullptr part. When will the pointer ever be null?
    The idea there is simply that a default-constructed iterant should behave something like a "nil object". Not really important for our purposes here, just an idiom I find useful.

  14. #14
    Registered User
    Join Date
    Dec 2017
    Posts
    1,633
    I see that with the precedence thing we were talking past each other for the most part. You are absolutely correct that the pointer version behaves as if the dereference happens first since it happens to the pre-incremented object. So if you were to expand x = *p++ you would rewrite it as x = *p; ++p, dereferencing the pointer first.
    A little inaccuracy saves tons of explanation. - H.H. Munro

  15. #15
    Registered User Sir Galahad's Avatar
    Join Date
    Nov 2016
    Location
    The Round Table
    Posts
    277
    Quote Originally Posted by john.c View Post
    I see that with the precedence thing we were talking past each other for the most part. You are absolutely correct that the pointer version behaves as if the dereference happens first since it happens to the pre-incremented object. So if you were to expand x = *p++ you would rewrite it as x = *p; ++p, dereferencing the pointer first.
    Well it is bound to happen sometimes. Thanks for the input, by the way. It really did help push me towards a more satisfactory solution. The "iterant functor" paradigm is very simple, which is great from the standpoint of someone working on an third-party library, as it should be much easier for users to conform to such an interface. No confusing C++ incantations, just define a few simple member functions and done!

Popular pages Recent additions subscribe to a feed

Similar Threads

  1. std::iterator::pointer and std::iterator::reference
    By etrusks in forum C++ Programming
    Replies: 4
    Last Post: 02-27-2016, 11:27 AM
  2. Replies: 19
    Last Post: 01-06-2012, 03:01 PM
  3. Connecting input iterator to output iterator
    By QuestionC in forum C++ Programming
    Replies: 2
    Last Post: 04-10-2007, 02:18 AM
  4. std::map::iterator
    By Magos in forum C++ Programming
    Replies: 2
    Last Post: 02-03-2006, 07:47 PM
  5. Hmm. A Quandary. Rounding/adding problem
    By Sennet in forum C++ Programming
    Replies: 11
    Last Post: 10-08-2005, 12:29 AM

Tags for this Thread