1

I have a legacy C API to a container-like object (specifically, the Python C API to tuples) which I would like to wrap in a nice C++14 API, so that I can use an iterator. How should I go about implementing this?

Here's some more details. We have the following existing C API which cannot be changed:

Py_ssize_t PyTuple_GET_SIZE(PyObject *p);
PyObject* PyTuple_GET_ITEM(PyObject *p, Py_ssize_t pos);
void PyTuple_SET_ITEM(PyObject *p, Py_ssize_t pos, PyObject *o)

We want to create a class which allows you to get access to a read/write iterator on the elements in the tuple.

The forward read-only iterator is not too difficult to define. Here is what I have:

class PyTuple {
private:
  PyObject* tuple;

public:
  PyTuple(PyObject* tuple) : tuple(tuple) {}

  class iterator {
    // iterator traits
    PyObject* tuple;
    Py_ssize_t index;
  public:
    iterator(PyObject *tuple, Py_ssize_t index) : tuple(tuple), index(index) {}
    iterator& operator++() { index++; return *this; }
    iterator operator++(int) { auto r = *this; ++(*this); return r; }
    bool operator==(iterator other) const { return tuple == other.tuple && index == other.index; }
    bool operator!=(iterator other) const { return !(*this == other); }
    PyObject* operator*() { return PyTuple_GET_ITEM(tuple, index); }
    // iterator traits
    using difference_type = Py_ssize_t;
    using value_type = PyObject*;
    using pointer = PyObject**;
    using reference = PyObject*&;
    using iterator_category = std::forward_iterator_tag;
  };

  iterator begin() {
    return iterator(tuple, 0);
  }

  iterator end() {
    return iterator(tuple, PyTuple_GET_SIZE(tuple));
  }
}

However, I am not too sure how to support writes. I have to somehow make *it = pyobj_ptr work. Conventionally, this would be done by changing the type to PyObject*& operator*() (so that it gives an lvalue) but I can't do this because the tuple "write" needs to go through PyTuple_SET_ITEM. I have heard that you can use operator= to solve this case but I am not sure if I should use a universal reference (Why no emplacement iterators in C++11 or C++14?) or a proxy class (What is Proxy Class in C++), and am not exactly sure what the code should look like exactly.

Ðаn
  • 10,934
  • 11
  • 59
  • 95
Edward Z. Yang
  • 26,325
  • 16
  • 80
  • 110

1 Answers1

3

What you're looking for is basically a proxy reference. You don't want to dereference into PyObject*, you want to dereference into something that itself can give you a PyObject*. This is similar to how the iterators for types like vector<bool> behave.

Basically, you want operator*() to give you something like:

class PyObjectProxy {
public:
    // constructors, etc.

    // read access
    operator PyObject*() const { return PyTuple_GET_ITEM(tuple, index); }

    // write access
    void operator=(PyObject* o) {
        PyTuple_SET_ITEM(tuple, index, o); // I'm guessing here
    }
private:
    PyObject* tuple;
    Py_ssize_t index;  
};
Barry
  • 286,269
  • 29
  • 621
  • 977
  • Thank you, that explains things nicely! I have a few extra questions: (1) in the SO link about emplacement iterators, it is mentioned that the C++ standard requires certain iterators NOT to use proxies. Why was this restriction levied? (2) I noticed for OutputIterator, the idiom is to define the `*` operator as `return *this`. IIUC, this doesn't work for reads? – Edward Z. Yang Jun 15 '17 at 19:41
  • To put it differently, the proxy object looks weirdly the same as the iterator object (field-wise), but I guess it is not possible to use the same thing in both cases? – Edward Z. Yang Jun 15 '17 at 19:42
  • @EdwardZ.Yang I don't understand either question, or how they're related to this one. It's better to use different types to solve different problems. – Barry Jun 15 '17 at 19:44
  • @edward you cannot create a conformant random access iterator that uses such proxy iterators. The same is true of many iterator categories, but not all. – Yakk - Adam Nevraumont Jun 15 '17 at 21:54