Nick Chapman
Nick Chapman

Reputation: 4634

Python-like dynamic argument unpacking in C++?

I have a std::map<int, std::vector<MyClass>> and a function that accepts a dynamic number of iterators, specifically iter::zip_longest.

I want to pass the vectors I have in the map to zip_longest, but given that there are a dynamic number of vectors in the map, I'm not seeing a straightforward way to achieve this. I know that it's doable with compile time constants using templates and sizeof, however I can't specify this at compile time.

What I essentially want to do is

std::vector<std::vector<MyClass>*> all_vectors;
for (const auto it : my_map) {
  all_vectors.push_back(&it.second);
}
for (const auto it : iter::zip_longest(*all_vectors)) {  // <-- Here using *all_vectors to unpack like in Python.
  // Do stuff here
}

Let me also say that I am aware that this can be achieved through other means than zip_longest, I'm just wondering if there is a clean way for me to use this existing function.

Upvotes: 4

Views: 575

Answers (2)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275790

The static type system of C++ is static. You have a dynamic number of elements in that vector. So you cannot have the static type system of C++ process the result, barring some fancy "generate all static types up to N then dispatch to it" that is reasonably inefficient and not all that wealdly.

zip_longest is not a function. It is a function template. A function template generates a function, often at the call site, by statically examining its arguments.

Now, it would be quite possible to create an iterator that, given a dynamic vector of containers, returns a dynamic vector of elements. The dynamic information in the source argument -- the number of elements -- corresponds to the dynamic elements in the type returned.

So something like this is possible:

std::vector<std::vector<MyClass>*> all_vectors;
for (const auto it : my_map) {
  all_vectors.push_back(&it.second);
}
for (const auto& elems : vec_zip_longest(all_vectors)) {
  // elems is a vector of boost::optional<std::ref<MyClass*>>
  // Do stuff here
}

doing this for the restricted case of just for(:) loops would look something like:

template<class C>
struct vec_zip_longest {
  using elem_iterator = decltype( std::begin( std::declval<C&>() ) );
  using elem = std::decay_t< decltype( *std::declval<elem_iterator&>() ) >;
  std::vector< std::optional<elem_iterator> > current;
  std::vector< elem_iterator > finish;

    vec_zip_longest( std::vector<C>& vec ) {
        for (auto&& c : vec )
        {
            current.emplace_back( std::begin(c) );
            finish.emplace_back( std::end(c) );
            if (current.back() == finish.back())
                current.back() = {};
        }
    }

  bool advance() {
    bool retval = false;
    for (std::size_t i = 0; i < current.size(); ++i) {
      auto& it = current[i];
      //std::cerr << "advancing\n";
      if (!it)
      {
        //std::cerr << "already done\n";
        continue;
      }
      ++*it;
      if (*it == finish[i]) {
        //std::cerr << "reached end\n";
        it = std::nullopt;
        continue;
      }
      //std::cerr << "advanced\n";
      retval = true;
    }
    return retval;
  }
  struct iterator {
    vec_zip_longest* parent = nullptr;
    friend bool operator==(iterator const& lhs, iterator const& rhs) {
      return lhs.parent == rhs.parent;
    }
    friend bool operator!=(iterator const& lhs, iterator const& rhs) {
      return lhs.parent != rhs.parent;
    }
    void operator++()& {
      if(!parent->advance())
        parent = nullptr;
    }
    auto operator*() {
      std::vector<std::optional<std::reference_wrapper<elem>>> retval;
      retval.reserve(parent->current.size());
      for(auto&& oit:parent->current) {
        //std::cerr << "getting element\n";
        if (oit) {
          //std::cerr << "something there\n";
          retval.emplace_back(**oit);
        } else {
          //std::cerr << "nothing there\n";
          retval.emplace_back();
        }
      }
      return retval;
    }
  };
  iterator begin() {
    return {this};
  }
  iterator end() {
    return {};
  }
};
template<class C>
vec_zip_longest(std::vector<C>&)->vec_zip_longest<C>;

in , Live example.

A "more proper" iterator would take a bit more boilerplate, and you could not technically make it stronger than an input iterator. Also having support for non-member begin/end takes more work which I skipped here.

Test code:

std::vector<std::vector<int>> foo{ {1,2,3}, {4,5}, {6} };
vec_zip_longest zip { foo };
for (auto&& c:zip)
{
    for (auto&& e : c ) {
        if (e)
            std::cout << *e << ",";
    }
    std::cout << "\n";
}

output is:

1,4,6,
2,5,
3,

Upvotes: 2

Nicol Bolas
Nicol Bolas

Reputation: 474106

C++ is a statically typed language. zip_longest is a function that produces a zip iterator based on the number of parameters that it gets. But the number of parameters a function gets is a static quantity in C++. If you need to give a function a runtime number of values, then you have to provide a runtime range or container or something of that sort. And zip_longest isn't built for that.

Upvotes: 3

Related Questions