1

Assume you have a std::tuple with a common base class:

class MyBase { public: virtual int getVal() = 0; };
class MyFoo1: public MyBase { public: int getVal() override { return 101; } };
class MyFoo2: public MyBase { public: int getVal() override { return 202; } };
using MyTuple = std::tuple<MyFoo1, MyFoo2, MyFoo1>;

How do you iterate over the elements of the tuple at runtime? The usual answer is that you can't because they all have different types, but here I'm happy for a static type of MyBase*. I'm hoping for code like this:

MyTuple t;
for (Base* b : iterate_tuple<MyBase>(t)) {
    std::cout << "Got " << b->getVal() << "\n";
}

There are a lot of helpful ideas over at How can you iterate over the elements of an std::tuple?, but they all include the code to run at each iteration in the fiddly template code, whereas I'd like all the fiddly template code bundled into the hypothetical iterate_tuple function so my code is just a normal for loop.

Arthur Tacca
  • 8,833
  • 2
  • 31
  • 49
  • 3
    If you’re using C++17 or higher you could try out `std::apply`. You can read more about it here, https://en.cppreference.com/w/cpp/utility/apply. – crdrisko Aug 19 '20 at 11:34
  • It seems that all that `iterate_tuple<>` needs to do is return a `std::vector` of pointers. That seems to be a much simpler, trivial solution than any of the other approaches. – Sam Varshavchik Aug 19 '20 at 12:19
  • @SamVarshavchik It would be a bit of a pity to allocate memory just to do the iteration. And I'm not convinced that calling a recursive template function that constructs a vector, then iterating over the vector, is more "trivial" compared to a classic integer loop that calls a recursive template function. – Arthur Tacca Aug 19 '20 at 12:23
  • Hmmm... Come to think of it, I think `std::array` would work just as well. No memory allocation. And I do not see any reason why a recursive function would be necessary. Working it out in my head, the array should be constructible in exactly one statement, using `...` in conjunction with `std::index_sequence` and `std::get`. Maybe a helper template, perhaps. – Sam Varshavchik Aug 19 '20 at 12:26
  • @SamVarshavchik I don't think you can use a fold expression (i.e. `...`) unless you're in a template function with a parameter pack. I'm not an expert though so could be wrong... but that just proves the point that relying on such tricks in application code is not ideal! Sounds like it does have potential to work in a helper function though. – Arthur Tacca Aug 19 '20 at 12:32
  • @crdrisko Thanks, that is definitely a potential answer in some circumstances. As I said in response to super's answer, I do still think it's more complex for a human reader than a simple `for` loop, partly because of the bracket soup, and it doesn't allow use of `break` and `return` in a natural way. – Arthur Tacca Aug 19 '20 at 12:34
  • That's why I finished my comment with "maybe a helper template, perhaps". – Sam Varshavchik Aug 19 '20 at 12:36
  • @SamVarshavchik Yes, I was referring to that part of your comment. – Arthur Tacca Aug 19 '20 at 12:40

5 Answers5

3

As mentioned and suggested in the question linked to using std::apply is a good way to get each individual element of the tuple.

Making a small helper function to wrap the forwarding of each tuple element makes it easy to use.

It's not the specific for-loop syntax you asked for, but it's as easy to follow if you ask me.

#include <tuple>
#include <utility>
#include <iostream>

class MyBase { public: virtual int getVal() = 0; };
class MyFoo1: public MyBase { public: int getVal() override { return 101; } };
class MyFoo2: public MyBase { public: int getVal() override { return 202; } };
using MyTuple = std::tuple<MyFoo1, MyFoo2, MyFoo1>;

template <typename Tuple, typename Callable>
void iterate_tuple(Tuple&& t, Callable c) {
    std::apply([&](auto&&... args){ (c(args), ...); }, t);
}

int main() {
    MyTuple t;
    iterate_tuple(t, [](auto& arg) {
        std::cout << "Got " << arg.getVal() << "\n";
    });

    iterate_tuple(t, [](MyBase& arg) {
        std::cout << "Got " << arg.getVal() << "\n";
    });
}

We can get the exact type by using auto or use the common base type.

super
  • 12,335
  • 2
  • 19
  • 29
  • That is quite nice, thanks. I think most C++ developers know about lambda functions, so it's not too complex of a solution. I do still think it is a more complex solution than a `for` loop though e.g. you have to think about what to capture (if anything), and for me all the brackets take a bit more mental effort to parse. It also doesn't play well with flow control e.g. if I want to `break` or `return` in the loop. – Arthur Tacca Aug 19 '20 at 12:29
2

Here's a little wrapper function that gets the tuple value by index, specified at runtime, which does a linear search for the right index by recursively calling itself with a different template parameter. You specify its return type as a template parameter, and the value gets implicitly converted to it.

template <class BaseT, class TupleT, size_t currentIndex = 0>
BaseT* getBasePtr(TupleT& t, size_t desiredIndex) {
    if constexpr (currentIndex >= std::tuple_size<TupleT>::value) {
        return nullptr;
    }
    else {
        if (desiredIndex == currentIndex) {
            return &std::get<currentIndex>(t);
        }
        else {
            return getBasePtr<BaseT, TupleT, currentIndex + 1>(t, desiredIndex);
        }
    }
}

You can then use it in a loop over the indices of the tuple:

for (size_t i = 0; i < std::tuple_size<MyTuple>::value; ++i) {
    MyBase* b = getBasePtr<MyBase>(t, i);
    std::cout << "At " << i << " got " << b->getVal() << "\n";
}

It's not quite as neat as a range-based for loop but it's still pretty straightforward to use. (You could wrap it in an iterator class that would support range-based loops but I don't really think it's worth the effort.)

Arthur Tacca
  • 8,833
  • 2
  • 31
  • 49
  • You may have already seen it (and deemed it to fiddly), but if note, note that the thread you link to does offer the somewhat neat approach `std::apply([](auto&&... args) {((std::cout << args.getVal() << '\n'), ...);}, t);`. – dfrib Aug 19 '20 at 11:30
  • @dfri Thanks for that, it's definitely an option, but I do consider it a bit less straightforward. I don't expect everyone on my team to know about parameter pack expansion, and even people that do (like me!) don't find them quite so easy to follow as a for loop. – Arthur Tacca Aug 19 '20 at 12:07
1

As Sam suggests in the comments, it's quite simple to create an array from a tuple.

template<typename Base, typename Tuple, size_t... Is>
std::array<Base *, std::tuple_size_v<Tuple>> iterate_tuple_impl(Tuple& tuple, std::index_sequence<Is...>)
{
    return { std::addressof(std::get<Is>(tuple))... };
}

template<typename Base, typename Tuple>
std::array<Base *, std::tuple_size_v<Tuple>> iterate_tuple(Tuple& tuple)
{
    return iterate_tuple_impl(tuple, std::make_index_sequence<std::tuple_size_v<Tuple>>{});
}
Caleth
  • 52,200
  • 2
  • 44
  • 75
0

If you have inheritance, why not to do without tuple and use inheritance capabilities like this:

    #include <iostream>
    #include <vector>

    class MyBase { public: virtual int getVal() = 0; };
    class MyFoo1 : public MyBase { public: int getVal() override { return 101; } };
    class MyFoo2 : public MyBase { public: int getVal() override { return 202; } };

    int main() {
        std::vector<std::unique_ptr<MyBase>> base;
        base.emplace_back(new MyFoo1);
        base.emplace_back(new MyFoo2);
        for (auto && derived : base) {
            std::cout << derived->getVal() << std::endl;
        }
    }   
gera verbun
  • 285
  • 3
  • 6
  • There are certainly use cases where you need the actual tuple of concrete types - perhaps you to do `std::get(t)` in one method, and iterate in another method (indeed that is exactly what I need to do). This answer will not help with those cases. – Arthur Tacca Aug 19 '20 at 12:27
  • 1
    And if you are going to use a vector of pointers, I would at least use a smart pointer like `std::unique_ptr` so that the memory is cleaned up safely. This example has a memory leak! – Arthur Tacca Aug 19 '20 at 12:28
0

I would directly use std::apply, but you can create array of Base*:

template <typename Base, typename Tuple>
std::array<Base*, std::tuple_size<Tuple>> toPtrArray(Tuple& tuple)
{
    return std::apply([](auto& ... args){ return std::array<Base*, std::tuple_size<Tuple>>{{&args}}; }, tuple);
}

And then

MyTuple t;
for (Base* b : toPtrArray<MyBase>(t)) {
    std::cout << "Got " << b->getVal() << "\n";
}
Jarod42
  • 203,559
  • 14
  • 181
  • 302