C++11: Обход элементов в кортеже (std::tuple)

August 3, 2015

Кортеж (std::tuple) появился в стандартной библиотеке начиная с С++11. Это неоднородный список элементов, типы которых задаются или выводятся на этапе компиляции. Кортеж “похож” на пару (std::pair), однако он может содержать произвольное (хотя и конечное) количество элементов. Интерфейс кортежа довольно простой: http://en.cppreference.com/w/cpp/utility/tuple

Создать кортеж можно так:

#include <tuple>

std::tuple<std::string, int, double> tuple_("hello", 42, 3.14);

Про кортеж говорят, что это коллекция гетерогенных значений (разнородных). Поэтому тип элемента (value_type), который определен в любом стандартном контейнере, не имеет смысла в кортеже, и просто отсутствует, ровно как и любой из типов итераторов. Кортеж не является обычным контейнерным классом, и не отвечает концепции контейнеров в С++ (http://en.cppreference.com/w/cpp/concept/Container). Поэтому простого способа обхода элементов кортежа нет. К нему не применимы какие-либо циклы:

for (auto& x : tuple_);  // Ошибка компиляции!

// error: no viable 'begin' function available

“Пройтись” по элементам кортежа можно только точно зная количество его аргументов на этапе компиляции. Для этого нужно использовать функцию get:

std::get<0>(tuple_); // индекс элемента в качестве аргумента шаблона
std::get<1>(tuple_);
std::get<2>(tuple_);

При этом передача индекса во время выполнения программы невозможна:

int i = 0;
std::get<i>(tuple_);  // Ошибка компиляции!

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

Для начала стоит привести пример, в каких случаях нам может понадобиться кортеж, и обход его элементов.

В стандарте С++11 появились вариативные шаблоны (variadic templates), и теперь мы можем, например, писать функции, которые способны принимать любое количество параметров разных типов.

// Шаблон функции с переменным числом параметров
template<typename... Args> 
void func(Args... args)
{
   // do something
}

// Использование
func("hello", 42, 3.14);
func(42);
func(); // и даже так...

Внутри функции к переданным значениям аргументов нужно “как-то обращаться”. И здесь нам поможет кортеж. Мы можем сконструировать подходящий кортеж, используя вспомогательную функцию std::make_tuple:

#include <utility> // std::forward

template<typename... Args>
void func(Args... args)
{
   // В данном случае auto - это std::tuple<Args...>
   auto tuple_ = std::make_tuple(std::forward<Args>(args)...);
}

Используя интерфейс кортежа, можно работать с данными, которые были переданы функции. Например, попробуем вывести в std::out все элементы кортежа (значения аргументов функции).

Но вот проблема: в зависимости от того, сколько параметров будет передано функции, кортеж будет содержать разное количество элементов. И не смотря на то, что мы можем узнать количество элементов кортежа, с помощью вспомогательной функции std::tuple_size, написать обобщенный код все равно не удастся. По крайней мере, без “шаблонной магии”, к которой далее мы и прибегнем.

Чтобы обработать каждый элемент кортежа, было бы удобно иметь функцию вида for_each, которая принимала бы сам кортеж и функцию-обработчик:

template<typename T>
void callback(int index, T t) 
{
    // processing
}

for_each(tuple_, callback); // Пройтись по всем элементам tuple_ и 
                            // вызвать для каждого функцию-обработчик callback

Чтобы создать такой for_each, нам потребуется придумать способ обхода элементов в кортеже. Кортежи впервые появились в библиотеке boost, и там, для обхода, применялась рекурсия. Тот же способ применим и мы:

#include <tuple>


namespace _detail_
{
    // Главная роль здесь у шаблона класса iterate_tuple.
    
    // Первый параметр шаблона класса iterate_tuple имеет тип int (index).
    // Значение этого параметра используется функцией get, 
    // которая "достает" из кортежа элемент по указанной позиции.
    
    // Мы рекурсивно сдвигаем позицию (уменьшаем index на 1) и таким образом
    // перемещаемся по кортежу.
    
    // Когда значение индекса становится равно 0, рекурсия завершается,
    // благодаря частичной специализации для индекса равного 0.
    
    // При этом есть особый случай, когда индекс равен -1. Он соответствует 
    // ситуации, когда кортеж не содержит ни одного элемента.
    
    template<int index, typename Callback, typename... Args>
    struct iterate_tuple 
    {
        static void next(std::tuple<Args...>& t, Callback callback) 
        {
            // Уменьшаем позицию и рекурсивно вызываем этот же метод 
            iterate_tuple<index - 1, Callback, Args...>::next(t, callback);
            
            // Вызываем обработчик и передаем ему позицию и значение элемента
            callback(index, std::get<index>(t));
        }
    };
    
    // Частичная специализация для индекса 0 (завершает рекурсию)
    template<typename Callback, typename... Args>
    struct iterate_tuple<0, Callback, Args...> 
    {
        static void next(std::tuple<Args...>& t, Callback callback) 
        {
            callback(0, std::get<0>(t));
        }
    };

    // Частичная специализация для индекса -1 (пустой кортеж)
    template<typename Callback, typename... Args>
    struct iterate_tuple<-1, Callback, Args...>
    {
        static void next(std::tuple<Args...>& t, Callback callback)
        {
            // ничего не делаем
        }
    };
}

//
// "Волшебный" for_each для обхода элементов кортежа (compile time!):
//
template<typename Callback, typename... Args>
void for_each(std::tuple<Args...>& t, Callback callback) 
{
    // Размер кортежа
    int const t_size = std::tuple_size<std::tuple<Args...>>::value;
    
    // Запускаем рекурсивный обход элементов кортежа во время компиляции
    _detail_::iterate_tuple<t_size - 1, Callback, Args...>::next(t, callback);
}

Пример кода хорошо задокументирован, и, надеюсь, понятен. Здесь нет сложных конструкций и он достаточно мал. Но это - всё что нужно, чтобы реализовать for_each для кортежа! А теперь пример использования:

#include <tuple>
#include <utility>  // std::forward
#include <iostream> // std::cout

// Код функции for_each опущен для простоты примера 
// (его лучше вынести в отдельный файл *.h)

struct callback
{
    template<typename T>
    void operator()(int index, T&& t) // index - это позиция элемента в кортеже
    {                                 // t - значение элемента
        std::cout << index << '=' << t << std::endl;
    }
};


template<typename... Args>
void func(Args... args)
{
   // Значения аргументов функции внутри кортежа 
   auto tuple_ = std::make_tuple(std::forward<Args>(args)...);
   
   // Обход элементов кортежа и вызвов обработчика 
   for_each(tuple_, callback());
}


int main(int argc, char** argv) 
{
    func("hello", 42, 3.14);
    func(42, 3.14);
    func("hello");
    func();
    
    return 0;
}

Всё просто! Стоит обратить внимание только на callback. Мы не можем определить его так:

template<typename T>
void callback(int index, T&& t) 
{
    // processing
}


for_each(tuple_, callback);
// error: could not deduce template argument

Потому что, тогда бы потребовалось точно знать тип T, в момент передачи функции-обработчика. А этого мы не знаем в общем случае. Но шаблонные функции-члены не обязаны быть явно инстанцированы в момент создания экземпляра класса, а значит, мы можем применять их в качестве обработчиков для for_each.