Как получить i-й элемент из std:: tuple, когда я не знаю во время компиляции?

У меня есть переменная i типа std::size_t и кортеж типа std::tuple. Я хочу получить i -й элемент кортежа. Я пробовал это:

// bindings... is of type const T&...
auto bindings_tuple = std::make_tuple(bindings...);
auto binding = std::tuple_element<i, const T&...>(bindings_tuple);

Но я получаю эту ошибку компиляции, говоря, что первый аргумент шаблона должен быть интегральным постоянным выражением:

error: аргумент шаблона типа типа < std::size_t '(aka' unsigned long ') не является интегральным постоянным выражением

Можно ли получить i -й элемент кортежа и как это сделать?


Я хотел бы сделать это без использования boost, если это возможно.

Ответы

Ответ 1

Вы не можете. Это не для кортежа. Если вам нужен динамический доступ к элементу, используйте std::array<T,N>, который почти идентичен std::tuple<T,...,T>, но дает динамический [i] -оператор; или даже полностью динамический контейнер, например std::vector<T>.

Ответ 2

Это возможно:

struct Functor 
{
    template<typename T>
    void operator()(T& t) const { std::cout << t << std::endl; }
};

template<std::size_t I = 0, typename FuncT, typename... Tp>
inline typename std::enable_if<I == sizeof...(Tp), void>::type
  for_index(int, std::tuple<Tp...> &, FuncT)
  { }

template<std::size_t I = 0, typename FuncT, typename... Tp>
inline typename std::enable_if<I < sizeof...(Tp), void>::type
  for_index(int index, std::tuple<Tp...>& t, FuncT f)
  {
    if (index == 0) f(std::get<I>(t));
    for_index<I + 1, FuncT, Tp...>(index-1, t, f);
  }

auto t = make_tuple(1, 2, "abc", "def", 4.0f);
int i = 2; // for example
for_index(i, t, Functor());

Этот код напечатает:

ABC

Рабочий пример на идеоне: образец

Ответ 3

Это, вероятно, не то, что хочет OP, но в любом случае, можно вернуть i-й элемент с использованием времени выполнения i, если вы возвращаете тип варианта, например boost::variant или boost::any,

#include <tuple>
#include <stdexcept>
#include <boost/variant.hpp>

template <size_t n, typename... T>
boost::variant<T...> dynamic_get_impl(size_t i, const std::tuple<T...>& tpl)
{
    if (i == n)
        return std::get<n>(tpl);
    else if (n == sizeof...(T) - 1)
        throw std::out_of_range("Tuple element out of range.");
    else
        return dynamic_get_impl<(n < sizeof...(T)-1 ? n+1 : 0)>(i, tpl);
}

template <typename... T>
boost::variant<T...> dynamic_get(size_t i, const std::tuple<T...>& tpl)
{
    return dynamic_get_impl<0>(i, tpl);
}

Например:

#include <string>
#include <iostream>

int main()
{
    std::tuple<int, float, std::string, int> tpl {4, 6.6, "hello", 7};

    for (size_t i = 0; i < 5; ++ i)
        std::cout << i << " = " << dynamic_get(i, tpl) << std::endl;

    return 0;
}

напечатает:

0 = 4
1 = 6.6
2 = hello
3 = 7
terminate called after throwing an instance of 'std::out_of_range'
  what():  Tuple element out of range.
Aborted

(Для boost::variant<T...> требуется g++ 4.7)

Ответ 4

Вопрос здесь, каким будет тип возвращаемого типа, если это будет возможно? Он должен быть известен во время компиляции, но кортеж может содержать элементы разных типов.

Предположим, что у нас есть набор из трех элементов:

auto tuple = std::make_tuple(10, "", A());
using tuple_type = decltype(tuple);

По-видимому, получение N-го элемента не имеет большого смысла. Какой бы он был? Он не известен до времени выполнения. Однако вместо того, чтобы получать N-й элемент, вы можете применить к нему функцию, учитывая, что все элементы поддерживают некоторый общий протокол:

void process(int n)
{
  if (n == 0)
    func(std::get<0>(tuple));
  else if (n == 1)
    func(std::get<1>(tuple));
  else if (n == 2)
    func(std::get<2>(tuple));
}

Этот код "динамически" обрабатывает элемент, учитывая индекс n. Общим протоколом в этом примере является функция func, которая может сделать что-то значимое со всеми возможными типами, используемыми в кортеже.

Однако писать такой код вручную - это утомительно, мы хотим сделать его более общим. Начнем с извлечения функции приложения, поэтому мы можем повторно использовать одну и ту же функцию process для разных функторов:

template<template<typename > class F>
void process(int n)
{
  if (n == 0)
  {
    using E = typename std::tuple_element<0, tuple_type>::type;
    F<E>::apply(std::get<0>(tuple));
  }
  else if (n == 1)
  {
    using E = typename std::tuple_element<1, tuple_type>::type;
    F<E>::apply(std::get<1>(tuple));
  }
  else if (n == 2)
  {
    using E = typename std::tuple_element<2, tuple_type>::type;
    F<E>::apply(std::get<2>(tuple));
  }
}

В этом случае F может быть реализовано как-то вроде:

// Prints any printable type to the stdout
struct printer
{
  static void apply(E e)
  {
    std::cout << e << std::endl;
  }
}

Давайте сделаем компилятор для создания всего этого кода, сделаем его общим:

constexpr static std::size_t arity = std::tuple_size<tuple_type>::value;
template<int N>
struct wrapper
{
  template<template<typename, typename ... > class F>
  static void apply_to(tuple_type& tuple, int idx)
  {
    if (idx)
      // Double recursion: compile and runtime.
      // Compile-time "recursion" will be terminated once
      // we reach condition N == tuple arity
      // Runtime recursion terminates once idx is zero.
      wrapper<N + 1>::template apply_to<F>(tuple, idx - 1);
    else
    {
      // idx == 0 (which means original index is equal to N).
      using E = typename std::tuple_element<N, tuple_type>::type;
      F<E>::apply(std::get<N>(tuple));
    }
  }
};

// Termination condition: N == arity.
template<>
struct wrapper<arity>
{
  template<template<typename, typename ... > class F>
  static void apply_to(tuple_type&, int)
  {
    // Throw exception or something. Index is too big.
  }
};

Использование:

wrapper<0>::template apply_to<printer>(tuple, 2);
Тем не менее, если сделать это полностью родовым, это еще одна история. По крайней мере, он должен быть независимым от типа кортежа. Затем вы, вероятно, захотите генерировать возвращаемый тип функтора, чтобы вы могли возвращать осмысленный результат. В-третьих, создание функции для принятия дополнительных параметров.

P.S. Я не настоящий разработчик на С++, поэтому вышеприведенный подход может быть полной бессмысленностью. Тем не менее, я счел это полезным для моего проекта микроконтроллера, где я хочу, насколько это возможно, решить во время компиляции и все же быть достаточно общим, чтобы я мог легко перемешать вещи. Например, "меню" в моем проекте в основном представляет собой набор "действий", каждое действие представляет собой отдельный класс, который поддерживает простой протокол, такой как "распечатать метку в текущей позиции на ЖК-дисплее" и "активировать и запускать петлю пользовательского интерфейса",.