0

In my application I generate a tuple with many vectors holding values. I would like to have a generic way of iterating over the tuple to extract the single vector with least values.

For example:

auto t = std::make_tuple(std::vector<int>({1,2}), std::vector<double>({1.0, 2.0, 3.0}));

How do I extract the vector which holds the least values?

NindzAI
  • 570
  • 5
  • 19

2 Answers2

3

Given a tuple with vectors holding different types how do I extract the vector with minimum size?

You can't You can't directly.

Because they are different types, the decision is based on values (not on types), so you can't decide the type extracted compile time (std::tuple can't be constexpr), and C++ is a strong typed language.

The best simplest thing you can do is extract the index of the vector with minimum size. Because in this case the value extracted is an integer (std::size_t, by example) and you can iterate the over the vectors in the tuple to select the one with less elements.

Different is if you have to extract the std::array with minimum size

auto t = std::make_tuple(std::array<int, 2u>({1,2}),
                         std::array<double, 3u>({1.0, 2.0, 3.0}));

because the size (2u for the first, 3u for the second) is know compile time so you can select the second array compile time.

If you can use C++17, you can use std::variant, so a type that can contain all types of your tuple.

As pointed by Caleth (thanks) if you can use only C++11/C++14 with boost libraries, you can use boost::variant. But I don't know it so I can't show you a specific example (but you can see the Caleth's answer)

The following is an example with a two-types tuple

template <typename T1, typename T2>
std::variant<T1, T2> getLess (std::tuple<T1, T2> const & tp)
 {
   std::variant<T1, T2>  v;

   if ( std::get<0>(tp).size() < std::get<1>(tp).size() )
       v = std::get<0>(tp);
   else
       v = std::get<1>(tp);

   return v;
 }

int main ()
 {
   std::vector<int>     vi {1, 2, 3};
   std::vector<double>  vd {1.0, 2.0};

   auto gl = getLess(std::make_tuple(vi, vd));
 }

This works with a two types tuple. But with a N-types tuple (with N high) become complicated because you can't write something as

auto min_length = std::get<0>(tp).size();
auto min_index  = 0u;

for ( auto ui = 1u ; ui < N ; ++ui )
   if ( std::get<ui>(tp).size() < min_length )
    {
       min_length = std::get<ui>(tp).size();
       min_index  = ui;
    }

because you can't pass to std::get<>() a run-time value as ui.

Same problem assigning the variant. You can't simply write

v = std::get<min_index>(tp);

because min_index is a run-time value.

You have to pass through a switch()

switch ( min_length )
 {
   case 0: v = std::get<0>(tp); break;
   case 1: v = std::get<1>(tp); break;
   case 2: v = std::get<2>(tp); break;
   case 3: v = std::get<3>(tp); break;
   case 4: v = std::get<4>(tp); break;

   // ...

   case N-1: v = std::get<N-1>(tp); break;
 };

or something similar.

As you can see it's complicated and more complicated become if you want that the getLess() function is a variadic one.

For the variadic case, the best I can imagine (but is a C++17 solution; see Caleth's answer for a C++11 solution) is to use an helper function and template folding as follows.

#include <tuple>
#include <vector>
#include <variant>
#include <iostream>

template <typename ... Ts, std::size_t ... Is>
auto getLessHelper (std::tuple<Ts...> const & tp,
                    std::index_sequence<0, Is...> const &)
 {
   std::variant<Ts...> var_ret  { std::get<0>(tp) };
   std::size_t         min_size { std::get<0>(tp).size() };

   ((std::get<Is>(tp).size() < min_size ? (var_ret = std::get<Is>(tp),
                                           min_size = std::get<Is>(tp).size())
                                        : 0u), ...);

   return var_ret;
 }

template <typename ... Ts>
auto getLess (std::tuple<Ts...> const & tp)
 { return getLessHelper(tp, std::index_sequence_for<Ts...>{}); }

int main ()
 {
   std::vector<int>     vi {1, 2, 3};
   std::vector<double>  vd {1.0, 2.0};
   std::vector<float>   vf {1.0f};

   auto gl = getLess(std::make_tuple(vi, vd, vf));

   std::cout << std::visit([](auto const & v){ return v.size(); }, gl)
       << std::endl; // print 1, the size() of vf
 }
max66
  • 65,235
  • 10
  • 71
  • 111
  • 1
    Once you have the index you can then get the vector, of course, but indeed it's going to require some type erasure - probably via a visitor and a variant. Probably cause to revisit the design requirements. – Lightness Races in Orbit Dec 17 '18 at 13:34
  • @LightnessRacesinOrbit could you clarify how that may be done? I did get as far as getting the index. – NindzAI Dec 17 '18 at 13:36
  • @LightnessRacesinOrbit - yes: `std::variant` is a way, in a sense, to return al types together. But require C++17 and this question is tagged C++11. – max66 Dec 17 '18 at 13:38
  • I just upgraded to g++ 6.5, so have partial c++17 support, but not std::variant unfortunately. – NindzAI Dec 17 '18 at 13:40
  • I have access to boost, so if I have to use it to higher degree than I have before I am a OK with it. – NindzAI Dec 17 '18 at 13:41
  • @max66 OP is already using Boost, which provides `boost::variant` (just as it provided `shared_ptr` before C++ did). Worst case you can make a tagged union yerself – Lightness Races in Orbit Dec 17 '18 at 13:50
  • @LightnessRacesinOrbit - OK: I'm correcting my answer using `std::variant` and C++17 (sorry but I not use boost). – max66 Dec 17 '18 at 13:51
  • @max66 To be clear, I didn't think any correction was necessary - I was just complimenting your answer by adding an observation about the OP's situation – Lightness Races in Orbit Dec 17 '18 at 13:53
  • @LightnessRacesinOrbit - well... a little correction is appropriate – max66 Dec 17 '18 at 14:09
  • @Caleth - Ok; thanks. I don't know `boost::variant` so I've added a C++17 example mentioning that exist `boost::variant`. – max66 Dec 17 '18 at 14:10
  • @max66 thank you very much for your answer. It was quite revealing, and taught me a new expansion trick. – NindzAI Dec 18 '18 at 01:54
  • @NindzAI - It's a pity you can't completely use C++17: as you can see, template folding is very useful. But the `map` is Caleth's answer is a valid alternative. – max66 Dec 18 '18 at 02:07
2

Let's assume we either have no duplicates in our pack of types, or a way of removing them, and also a C++11 backport of std::index_sequence

#include <map>
#include <tuple>
#include <functional>
#include <iostream>

#include <boost/variant>
#include <future/index_sequence>

namespace detail {
    template<typename Variant, typename Tuple, std::size_t... Is>
    std::map<std::size_t, std::function<Variant(const Tuple &)>> from_tuple_map(index_sequence<Is...>)
    {
        return { { Is, [](const Tuple & tup){ return std::get<Is>(tup); } }... };
    }

    struct GetSize
    {
        template<typename T>
        std::size_t operator()(const std::vector<T> & vec){ return vec.size(); }
    }; // becomes C++14 generic lambda
}

template<typename... Ts>
boost::variant<Ts...> from_tuple(const std::tuple<Ts...> & tup)
{
    auto map = detail::from_tuple_map<boost::variant<Ts...>, std::tuple<Ts...>>(make_index_sequence<sizeof...(Ts)>());
    auto get_size = GetSize{};
    std::size_t best = 0;
    std::size_t len = visit(get_size, map[best](tup));
    for (std::size_t trial = 1; trial < sizeof...(Ts); ++trial)
    {
        std::size_t trial_len = visit(get_size, map[trial](tup));
        if (trial_len > len)
        {
            best = trial;
            len = trial_len;
        }
    }
    return map[best](tup);
}

int main()
{
    auto x = from_tuple(std::make_tuple(std::vector<int>({1,2}), std::vector<double>({1.0, 2.0, 3.0})));
    visit([](const auto & a){ std::cout << a[1]; }, x);
}

See it live (using C++17 compiler)

Caleth
  • 52,200
  • 2
  • 44
  • 75
  • @caleth thank you for the answer. The map trick is great, unfortunately it won't work in g++ 6.5 (compiler bug). I'll try upgrading to 7.4 to see if it got fixed. – NindzAI Dec 18 '18 at 01:57