4

Does anyone know of a good, clean way to iterate over a tuple in C++17 / 20? Let's say we have a bit of code like this:

class Test
{
    public:
        Test( int x ) : x_(x) {};
        void Go() const { std::cout << "Hi!" << x_ << "\n" ; }
        int x_;
};
int main()
{
    std::tuple tplb{ Test{1} , Test{2} ,  Test{3} };
}

How could we iterate through the tuple and call the Go() method on each using the latest 17/20 features?

I know you could just have a vector of the object and then it works easily. My goal with this is to be able to ha e a certain polymorphism without having to use virtual functions.

The idea would be to be able to have other object types in the tuple that support the same method. If the method is present in each object then the code would compile and execute without having to use a base-class, virtuals, vtable, etc.

Is there some way perhaps with std::apply or std::invoke ?

Brian Bi
  • 111,498
  • 10
  • 176
  • 312
JoshK
  • 337
  • 4
  • 16
  • 1
    @mistertribs that question asks about C++11, so it's now out of date. – Brian Bi Feb 11 '19 at 23:06
  • @Brian - nevertheless several of the answers there apply to C++17. –  Feb 11 '19 at 23:06
  • Yeah, I think/hope that there are some new constructs in 17/21 that make this really elegant. How cool would it be to have polymorphism like that enforced at compile time without virtuals? And without five pages of hard-to-read-template work. – JoshK Feb 11 '19 at 23:07
  • @mistertribs I agree, and in particular the answer by xskxzr explicitly targets C++17. But I don't think this question should be marked a duplicate of that one. – Brian Bi Feb 11 '19 at 23:08
  • I did read that one - I hope there are some better c++17/21 solutions that allow the direct calling of a method on the objects in the tuple. – JoshK Feb 11 '19 at 23:11
  • Would be nice if you could use range-based for loop but sadly it ain't so – M.M Feb 12 '19 at 01:12

2 Answers2

10

Is there some way perhaps with std::apply or std::invoke ?

std::apply fit the need indeed with fold expression:

std::tuple tplb{ Test{1} , Test{2} ,  Test{3} };

std::apply([](const auto&... tests){(tests.Go(), ...);}, tplb);

Here, we call method Go() for every type value of the tuple.

The idea would be to be able to have other object types in the tuple that support the same method. If the method is present in each object then the code would compile and execute without having to use a base-class, virtuals, vtable, etc.

So above method works. If you would go further and dispatch to different implementation according to type, you might use overloaded class from std::visit's example:

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

auto f = overloaded {
        [](const Test& test) { test.Go(); },
        [](double d) { std::cout << d << ' '; },
        [](const std::string& s) { std::cout << s << ' '; },
    };
std::apply([&](const auto&... e){ (f(e), ...);}, my_tuple);
Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • Ty, this is awesome! Minor note, this: "std::apply([](const auto&... tests){(test.Go(), ...);}, tplb);" needs to be "std::apply([](const auto&... tests) {(tests.Go(), ...); }, tplb);". – JoshK Feb 12 '19 at 02:33
  • This lets me use a second class is completely unrelated but implements the "Go()" method. So cool, ty again. – JoshK Feb 12 '19 at 02:34
  • What is the purpose of the `using` declaration `Ts::operator()...`? Since the `Ts...` are being publicly derived from, isn't it unnecessary to adjust the accessibillity of the `operator()` member function? – 303 Aug 18 '21 at 21:55
  • It also seems that `f` should be invoked within the fold expression instead of `overloaded`. Currently, this attempts to derive from whatever is contained by `my_tuple`, followed by disregarding the constructed result. – 303 Aug 18 '21 at 22:05
  • @303: Without `using Ts::operator()...;`, call would be ambiguous. – Jarod42 Aug 19 '21 at 06:45
  • Indeed, `f` should be used in `std::apply`, typo fixed. – Jarod42 Aug 19 '21 at 06:47
  • Interestingly, someone posted a question about this particular peculiarity just a couple weeks ago which you also happened to have answered. More information about this ambiguity issue: https://stackoverflow.com/questions/68678971/c-using-declaration-and-overload-paradigm – 303 Aug 19 '21 at 18:26
0

Bit of std::index_sequence trickery is needed to access each member of the tuple.

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

class Test
{
    public:
        Test( int x ) : x_(x) {}; 
        void Go() const { std::cout << "Hi!" << x_ << "\n" ; } 
        int x_; 
};



template<typename F, typename T, std::size_t... Index>
void doStuffIndex(F&& action, T const& tup, std::index_sequence<Index...> const&)
{
    bool ignore[] = {((std::get<Index>(tup).*action)(), true)...};
}

template<typename F, typename... Obj>
void doStuff(F&& action, std::tuple<Obj...> const& tup)
{
    doStuffIndex(action, tup, std::make_index_sequence<sizeof...(Obj)>());
}

int main()
{
    std::tuple<Test, Test, Test> tplb{ Test{1} , Test{2} ,  Test{3} };
    doStuff(&Test::Go, tplb);
}
Martin York
  • 257,169
  • 86
  • 333
  • 562
  • Ty for this but not what I meant to ask. Look at the answer below, that gets it exactly. Notice that in your solution you still are referencing a specific Class::Method(). That restricts the ability to call the same method on a different class. The answer below from Jarod42 nails it. – JoshK Feb 12 '19 at 02:35