0

I'm somewhat new to SWIG and C++, so this may be obvious, but I cannot seem to figure it out. I have a C++ collection object (in the MWE, a Pantry), that in C++ is indexed by a std::string, but iterates over the objects. This works in C++ using a ranged-based for loop with the implementations of begin() and end().

Here's a MWE:

pantry.i

%module(directors="1") my_collection
%include "stdint.i"
%include "std_string.i"
%include "std_shared_ptr.i"
%include "std_vector.i"
%{
#include "my_collection.cpp"
%}
%nodefaultctor;
%shared_ptr(Food);
%include "my_collection.cpp"

pantry.cpp

#pragma once

#include <string>
#include <vector>
#include <map>
#include <memory>
#include <sstream>

#ifdef SWIG

%define TO_STRING()
    %extend {
        %feature("python:slot", "tp_repr", functype="reprfunc") to_string;
    }
    std::string to_string()
%enddef

%define SUBSCRIPT(return_type, subscript_type)
    %feature("python:slot", "mp_subscript", functype="binaryfunc") __getitem__;
    %extend {
        return_type __getitem__(subscript_type key) {
            return (*($self))[key];
        };
    };
    return_type operator[](subscript_type key)
%enddef

#else

#define TO_STRING() \
        std::string to_string()

#define SUBSCRIPT(return_type, subscript_type) \
        return_type operator[](subscript_type key)

#endif


class Food {
public:

    std::string name;

    Food(std::string name) {
        this->name = name;
    };

    TO_STRING() {
        std::ostringstream stream;
        stream << "<Food " << this->name << ">";
        return stream.str();
    };

};

class Pantry {
private:

    // _foods is not grammatically correct
    std::map<std::string, std::shared_ptr<Food>> _food_map;
    std::vector<std::shared_ptr<Food>> _food_vec;

public:
    Pantry() {
    };

    std::shared_ptr<Food> add(std::string name) {
        auto food = std::shared_ptr<Food>(new Food(name));
        this->_food_map[food->name] = food;
        this->_food_vec.push_back(food); // p.s., how do I prevent making a copy?
        return food;
    };

    SUBSCRIPT(std::shared_ptr<Food>, std::string) {
        return this->_food_map.at(key);
    };

    TO_STRING() {
        return "<Pantry>";
    };

    std::vector<std::shared_ptr<Food>>::const_iterator begin() {
        return this->_food_vec.begin();
    };

    std::vector<std::shared_ptr<Food>>::const_iterator end() {
        return this->_food_vec.end();
    };

    size_t size() {
        return this->_food_vec.size();
    };

};

I've also put together a short Python script:

import pantry
pant = mc.Pantry()
print(pant.add('Bacon'))
print(pant.add('Cheese'))
print('subscript[Bacon]: {}'.format(pant['Bacon']))
for food in pant:
    print('iter: ... {}'.format(food))

Then, depending on whether the -builtin flag is passed to SWIG, I get the error:

  • without -builtin: TypeError: in method 'Pantry___getitem__', argument 2 of type 'std::string' -- which makes sense because I've chosen to index by strings
  • with -builtin: TypeError: 'my_collection.Pantry' object is not iterable -- I think this is caused by SWIG not implicitly creating methods I haven't declared, which is seems correct in this instance.

How can I iterate over the collection contents without explicitly calling a method? I realize I could just make the _food_vec public and iterate over that, but my actual code is more complicated and I'd prefer not to do that. Likewise, I could implement some other .iterate() method, but I've already done the work with begin() and end().

Edit: What would be a target-language agnostic method that I could implement similar to the macros I made for TO_STRING() and SUBSCRIPT, and what would be the correct template to add to the interface file (something like %template(FoodIterator) std::vector<Food>::iterator;?

SimplyKnownAsG
  • 904
  • 9
  • 26

1 Answers1

0

You have two options to implement iterable interface:

  1. Implement __iter__() which returns an object instance having next().
  2. Implement __getitem__() which accepts integers or slice objects.

I guess this should explain both errors as well as give a clue of how to fix.

Community
  • 1
  • 1
Alexander Solovets
  • 2,447
  • 15
  • 22
  • The iterator returned by `__iter__()` should also have a `__next__()` method to make sure it works in both 2 and 3 – Alec Dec 28 '15 at 03:36
  • I think #2 is the only option, since I've intentionally indexed by strings. I think you are correct that I need to implement a `__getitem__()` method, but I am struggling to understand how to do that in a target-language agnostic way. I also added an explanation for the errors, or at least my understanding of them. – SimplyKnownAsG Dec 28 '15 at 03:40
  • There's one more thing. In Python the iterator instance is independent of its container instance, so e.g. when you write the code like `for food in Pantry()` an instance of `Pantry` will be created and deleted right after the inner call to `Pantry.__iter__()`. Thus if your C++ iterator implementation simply holds `std::map::iterator` then you'll get a segfault. That said I see the only safe and easy way to make your collection iterable in copying the underlying collection and let the SWIG generate the iterator for you. – Alexander Solovets Jan 02 '16 at 22:56