Get first element of std::tuple satisfying trait

Viewed 790

I'm using C++17. I'd like to get an element of a tuple that satisfies some type trait. It would be amazing if the trait could be supplied generically, but I'd be satisfied with a specific function for a certain trait. Usage might look something like this:

auto my_tuple = std::make_tuple { 0.f, 1 };

auto basic = get_if_integral (my_tuple);
auto fancy = get_if<std::is_floating_point> (my_tuple);

std::cout << basic; // '1'
std::cout << fancy; // '0.f'

Ideally this would fail to compile if more than one element satisfies the trait, like std::get (std::tuple).

3 Answers

If I understand correctly what you want... I propose an helper struct gf_h ("get first helper") as follows

template <std::size_t, bool ...>
struct gf_h
 { };

template <std::size_t I, bool ... Bs>
struct gf_h<I, false, Bs...> : public gf_h<I+1u, Bs...>
 { };

template <std::size_t I, bool ... Bs>
struct gf_h<I, true, Bs...> : public std::integral_constant<std::size_t, I>
 { };

and a couple of functions that use it:

template <typename ... Us,
   std::size_t I = gf_h<0, std::is_integral<Us>::value...>::value>
auto get_first_integral (std::tuple<Us...> const & t)
 { return std::get<I>(t); }

template <typename ... Us,
   std::size_t I = gf_h<0, std::is_floating_point<Us>::value...>::value>
auto get_first_floating (std::tuple<Us...> const & t)
 { return std::get<I>(t); }

Observe that are SFINAE enabled/disabled functions, so are enabled only if there is an integral (or float) value in the tuple

The following is a full compiling example

#include <tuple>
#include <iostream>

template <std::size_t, bool ...>
struct gf_h
 { };

template <std::size_t I, bool ... Bs>
struct gf_h<I, false, Bs...> : public gf_h<I+1u, Bs...>
 { };

template <std::size_t I, bool ... Bs>
struct gf_h<I, true, Bs...> : public std::integral_constant<std::size_t, I>
 { };

template <typename ... Us,
   std::size_t I = gf_h<0, std::is_integral<Us>::value...>::value>
auto get_first_integral (std::tuple<Us...> const & t)
 { return std::get<I>(t); }

template <typename ... Us,
   std::size_t I = gf_h<0, std::is_floating_point<Us>::value...>::value>
auto get_first_floating (std::tuple<Us...> const & t)
 { return std::get<I>(t); }

int main()
 {
   auto tup1 = std::make_tuple(3.f, 2., 1, 0);

   std::cout << get_first_integral(tup1) << std::endl; // 1
   std::cout << get_first_floating(tup1) << std::endl; // 3

   auto tup2 = std::make_tuple("abc", 4, 5);

   std::cout << get_first_integral(tup2) << std::endl; // 4
   // std::cout << get_first_floating(tup2) << std::endl; // error

   auto tup3 = std::make_tuple("xyz", 6., 7.f);

   // std::cout << get_first_integral(tup3) << std::endl; // error
   std::cout << get_first_floating(tup3) << std::endl; // 6
 }

Here's a surprisingly simple way without using recursion:

template <template <typename...> typename T, typename... Ts>
constexpr int index_of_integral(const T<Ts...>&)
{
    const bool a[] = { std::is_integral_v<Ts>... };
    for (int i = 0; i < sizeof...(Ts); ++i) if (a[i]) return i;
    return -1;
}

template <typename T>
constexpr decltype(auto) get_if_integral(T&& t)
{
    return std::get<index_of_integral(t)>(std::forward<T>(t));
}

int main()
{
    constexpr auto t = std::make_tuple(3.14, 42, "xyzzy");
    static_assert(get_if_integral(t) == 42);
}

It could easily be extended to be parametrized on the trait.

The only things that make it C++17 are the is_integral_v variable template and the single-argument static_assert. Everything else is C++14.

Note that in C++20 the for loop could be replaced with std::find and std::distance.

Ideally it should throw an exception instead of returning -1, but compilers don't seem to like that.

Inspired by this answer.

Ok, I figured out a way to accomplish this in a way that is not generic over the trait, but that's good enough for my current purpose. Using if constexpr this really doesn't look too bad. I'm sure this isn't hugely idiomatic, but it works for me:

template <std::size_t Idx, typename... Us>
auto& get_if_integral_impl (std::tuple<Us...>& t)
{
    static_assert (Idx < std::tuple_size_v<std::tuple<Us...>>,
                   "No integral elements in this tuple.");

    if constexpr (std::is_integral<std::tuple_element_t<Idx, std::tuple<Us...>>>::value)
        return std::get<Idx> (t);
    else
        return get_if_integral_impl<Idx + 1> (t);
}

template<typename... Us>
auto& get_if_integral (std::tuple<Us...>& t)
{
    return get_if_integral_impl<0> (t);
}

auto tup = std::make_tuple (3.f, 2., 1, 0);
std::cout << get_if_integral (tup); // '1'

My use case is a little more complex, involving returning the first nested tuple which itself contains another type, but this should convey the basic idea.

Related