Работа с кортежами С++ (std::tuple). Функции foreach, map и call

в 15:32, , рубрики: c++, call, foreach, map, tuple, variadic templates, Программирование

Здесь я расскажу о работе с кортежами C++ (tuple), приведу несколько полезных функций, которые в состоянии существенно облегчить жизнь при использовании кортежей, а также приведу примеры использования этих функций. Всё из личного опыта.

Foreach

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

namespace tuple_utils
{
    // вызвать 'callback' для каждого элемента кортежа
    /*
        struct callback
        {
            template<std::size_t, class T>
            void operator()( T&& element )
            {
                // do something
            }
        };
        tupleForeach( callback(), myTuple );
    */
    template<class TCallback, class ...TParams>
    void tupleForeach( TCallback& callback, const std::tuple<TParams...>& tuple );

    namespace
    {

        template<std::size_t Index, class TCallback, class ...TParams>
        struct _foreach_
        {
            static void tupleForeach_( TCallback& callback, const std::tuple<TParams...>& tuple )
            {
                // такой пересчёт необходим для выполнения callback'a над элементами в порядке их следования
                const std::size_t idx = sizeof...( TParams ) - Index;
                callback.operator()<idx>( std::get<idx>( tuple ) );
                _foreach_<Index - 1, TCallback, TParams...>::tupleForeach_( callback, tuple );
            }
        };

        template<class TCallback, class ...TParams>
        struct _foreach_<0, TCallback, TParams...>
        {
            static void tupleForeach_( TCallback& /*callback*/, const std::tuple<TParams...>& /*tuple*/ ) {}
        };

    } //
    template<class TCallback, class ...TParams>
    void tupleForeach( TCallback& callback, const std::tuple<TParams...>& tuple )
    {
        _foreach_<sizeof...( TParams ), TCallback, TParams...>::tupleForeach_( callback, tuple );
    }

} // tuple_utils

Здесь используется вспомогательная структура _foreach_, имеющая в качестве дополнительного template-параметра очередной индекс кортежа. Единственный её статический метод tupleForeach_ вызывает для элемента с этим индексом заданную через callback функцию, после чего вызывается рекурсивно. Частичная специализация данной структуры для индекса, равного нулю, вырождена и является завершением рекурсии.

Пример 1. Банальный

struct ForeachCallback
{
    template<std::size_t Index, class T>
    void operator()( T&& element )
    {
        std::cout <<  "( " << Index << " : " << element << " ) ";
    }
};

void foo()
{
    auto myTyple = std::make_tuple( 42, 3.14, "boo" );
    tuple_utils::tupleForeach( ForeachCallback(), myTyple );
}

Пример 2. Проверка getter'ов

// определим тип getter'а как константный метод без параметров
template<class TResult, class TOwner>
using TGetter = TResult( TOwner::* )() const;

// класс, хранящий getter'ы одного объекта
template<class TGetterOwner, class ...TParams>
class MyGetterContainer
{
    // определим тип getter'а для объекта заданного класса
    template<class TResult>
    using TMyGetter = TGetter<TResult, TGetterOwner>;
    
    .....

    private:
    
        .....

        // проверить, нет ли среди getter'ов вырожденных (значения nullptr)
        void checkGetters();

        // кортеж getter'ов разных типов (т.е. возвращающих значения разных типов)
        std::tuple<TMyGetter<TParams>...> m_getters;
        
};

namespace
{
    // callback, выполняющий проверку каждого getter'а
    template<class TGetterOwner>
    struct GetterCheckCallback
    {
        // непосредственно функция проверки, которая будет вызвана для каждого getter'а
        //  здемь мы не используем 'Index' и действуем одинаково для всех элементов
        template<std::size_t Index, class T>
        void operator()( const TGetter<T, TGetterOwner>& element )
        {
            assert( element != nullptr );
        }
    };
} //
template<class TGetterOwner, class ...TParams>
void MyGetterContainer<TGetterOwner, TParams...>::checkGetters()
{
    // вызываем callback для проверки всех getter'ов
    tuple_utils::tupleForeach( GetterCheckCallback<TGetterOwner>(), m_getters );
}

Map

Ещё одна приходящая в голову задача для удобства работы с кортежами — функция, строящая новый кортеж последовательно из результатов выполнения заданной функции над элементами данного кортежа. Весьма распространённая в функциональных языках задача. Её реализация, пожалуй, ещё проще:

namespace tuple_utils
{
    // сформировать новый кортеж (кортеж другого типа) из результатов вызова 'callback'а для каждого из элементов кортежа 'sourceTuple'
    /*
        struct callback
        {
            template<std::size_t, class R, class T>
            R operator()( T&& element )
            {
                // do something
            }
        };
        mapTuple( callback(), myTuple );
    */
    template<class TCallback, class TSourceTuple>
    auto mapTuple( TCallback& callback, const TSourceTuple& sourceTuple );

    namespace
    {

        template<class TCallback, class TSourceTuple, std::size_t... Indices>
        auto mapTuple_( TCallback& callback, const TSourceTuple& sourceTuple, std::index_sequence<Indices...> )
        {
            return std::make_tuple( callback.operator()<Indices>( std::get<Indices>( sourceTuple ) )... );
        }

    } //
    template<class TCallback, class TSourceTuple>
    auto mapTuple( TCallback& callback, const TSourceTuple& sourceTuple )
    {
        return mapTuple_( callback, sourceTuple, std::make_index_sequence<std::tuple_size<TSourceTuple>::value>() );
    }
} // tuple_utils

Здесь используется вспомогательная функция mapTuple_, принимающая дополнительный параметр, набор всех индексов кортежа, через index_sequence. Из результатов выполнения заданной через callback функции над элементами по каждому из индексов формируется результирующий кортеж.

Пример 1. Банальный

struct MapCallback
{
    template<std::size_t Index, class T>
    std::string operator()( T&& element )
    {
        std::stringstream ss;
        ss <<  "( " << Index << " : " << element << " )";
        
        std::string result;
        result << ss;
        
        return result;
    }
};

void foo()
{
    auto sourceTyple = std::make_tuple( 42, 3.14, "boo" );
    auto strTuple = tuple_utils::mapTuple( MapCallback(), sourceTyple );
}

Пример 2. Формирование кортежа значений getter'ов

// определим тип getter'а как константный метод без параметров
template<class TResult, class TOwner>
using TGetter = TResult( TOwner::* )() const;

// класс, хранящий getter'ы одного объекта
template<class TGetterOwner, class ...TParams>
class MyGetterContainer
{
    // определим тип getter'а для объекта заданного класса
    template<class TResult>
    using TMyGetter = TGetter<TResult, TGetterOwner>;
    
    .....

    protected:
    
        .....

        // получить значения всех getter'ов
        std::tuple<TParams...> getterValues() const;

    private:
    
        .....
        // сам объект, у которого будут вызываться getter'ы
        TGetterOwner& m_getterOwner;
        // кортеж getter'ов разных типов (т.е. возвращающих значения разных типов)
        std::tuple<TMyGetter<TParams>...> m_getters;
};

namespace
{
    // callback, возвращающий значения getter'ов
    template<class TGetterOwner>
    struct GetterValuesCallback
    {
        public:

            // конструктор
            GetterValuer( const TGetterOwner& getterOwner ) :
                m_getterOwner( getterOwner )
            {
            }

            // непосредственно функция, возвращающая значение getter'а; она будет вызвана для каждого getter'а
            //  здемь мы не используем 'Index' и действуем одинаково для всех элементов
            template<std::size_t Index, class T>
            T operator()( const TGetter<T, TGetterOwner>& oneGetter )
            {
                return ( m_getterOwner.*oneGetter )();
            }

        private:

            const TGetterOwner& m_getterOwner;
    };
} //
template<class TGetterOwner, class ...TParams>
std::tuple<TParams...> MyGetterContainer<TGetterOwner, TParams...>::getterValues() const
{
    // вызываем callback для формирования нового кортежа из возвращённых значений всех getter'ов
    return tuple_utils::mapTuple( GetterValuesCallback<TGetterOwner>( m_getterOwner ), m_getters );
}

Call

Что ещё хотелось бы «уметь делать» с кортежами — это использовать их содержимое как параметры вызова какой-либо функции (естественно, порядок и тип аргументов которой соответствует порядку и типу элементов кортежа). Реализация данной функции весьма схожа с реализацией функции map:

namespace tuple_utils
{
    // вызвать 'callback', принимающий в качестве параметров распакованный кортеж 'tuple'
    /*
        struct callback
        {
            template<class TResult, class ...TParams>
            TResult operator()( TParams... )
            {
                // do something
            }
        };
        callTuple( callback(), myTuple );
    */
    template<class TCallback, class ...TParams>
    auto callTuple( TCallback& callback, const std::tuple<TParams...>& tuple );

    namespace
    {

        template<class TCallback, class TTuple, std::size_t... Indices>
        auto callTuple_( TCallback& callback, const TTuple& tuple, std::index_sequence<Indices...> )
        {
            return callback( std::get<Indices>( tuple )... );
        }

    } //

    template<class TCallback, class ...TParams>
    auto callTuple( TCallback& callback, const std::tuple<TParams...>& tuple )
    {
        return callTuple_( callback, tuple, std::index_sequence_for<TParams...>() );
    }
} // tuple_utils

Здесь, как и в случае с map, используется вспомогательная функция callTuple_, принимающая дополнительный параметр, набор всех индексов кортежа, через index_sequence. Она вызывает заданную через callback функцию, передавая ей все элементы кортежа, соответствующие индексам. Результатом её выполнения является результат выполнения переданной функции.

Пример 1. Банальный

bool checkSomething( int a, float b, const std::string& txt );

struct CallCallback
{
    template<class TResult, class ...TParams>
    TResult operator()( TParams... params )
    {
        return checkSomething( params... );
    }
};

void foo()
{
    std::tuple<int, float, std::string> paramsTyple = std::make_tuple( 42, 3.14, "boo" );
    bool isParamsValid = tuple_utils::callTuple( CallCallback(), paramsTyple );
}

Пример 2. Вызов setter'а с параметрами-значениями getter'ов

// класс, хранящий getter'ы одного объекта
template<class TGetterOwner, class ...TParams>
class MyGetterContainer
{
    .....

    protected:
    
        .....

        // получить значения всех getter'ов
        std::tuple<TParams...> getterValues() const;
        
    .....
};

// определим тип setter'а как неконстантный void-метод c параметрами
template<class TOwner, class ...TParams>
using TSetter = void( TOwner::* )( TParams... );

// класс, вызывающий setter одного объекта со значениями getter'ов другого
template<class TSetterOwner, class TGetterOwner, class ...TParams>
class MySetterCaller : public MyGetterContainer<TGetterOwner, TParams...>
{
    // определим тип getter'а для объекта заданного класса с заданными параметрами
    using TMySetter = TSetter<TSetterOwner, TParams...>;
    
    .....
    
    public:
    
        .....
        
        // вызвать setter со значениями getter'ов
        void callSetter();
    
    private:
    
        .....
        
        // сам объект, у которого будет вызываться setter
        TSetterOwner& m_setterOwner;
        // непосредственно setter
        TMySetter m_setter;
};

namespace
{
    // callback, выполняющий вызов setter'а
    template<class TSetterOwner, class ...TParams>
    struct CallSetterCallback
    {
        public:

            // конструктор
            GetterPasser( TSetterOwner& setterOwner, TSetter<TSetterOwner, TParams...> setter  ) :
                m_setterOwner( setterOwner ), m_setter( setter )
            {
            }

            // непосредственно функция, выполняющая вызов setter'а
            void operator()( TParams... params )
            {
                return ( m_setterOwner.*m_setter )( params... );                
            }

        private:

            TSetterOwner& m_setterOwner;
            TSetter<TSetterOwner, TParams...> m_setter;
    };
} //
template<class TSetterOwner, class TGetterOwner, class ...TParams>
void MySetterCaller<TSetterOwner, TGetterOwner, TParams...>::callSetter()
{
    // получим кортеж значений от getter'ов
    std::tuple<TParams...> _getterValues = getterValues();

    // вызываем callback для вызова setter'а от полученных значений
    tuple_utils::callTuple( CallSetterCallback( m_setterOwner, m_setter ), _getterValues );
}

P.S. В С++17 будет доступен std::apply, который выполняет тот же функционал.

Общие замечания

  • Передача индекса в callback
    В реализациях выше в callback передавался индекс элемента, который должен быть обработан callback'ом. Причём передавался он не в качестве аргумента, а в качестве параметра самого callback'а. Сделано именно так для расширения области применения функций, т.к. такая передача позволяет вызывать внутри callback'а шаблонные функции (использующие индекс в качестве параметра) и вообще использовать индекс как параметр для инстанцирования чего угодно, что было бы невозможно при передаче индекса как аргумента функции.
  • Передача callback
    Во всех реализациях выше передача callback'а осуществляется по ссылке (а не по значению). Это сделано для того, чтобы временем жизни конкретного используемого callback'а (а не его копии) управляла вызывающая сторона.
Ещё один пример. Преобразование обработчиков-методов в обработчики-функторы

// определим тип обработчика-метода как void-метод с одним параметром
template<class TObject, class TValue>
using TMethodHandler = void( TObject::* )( const TValue& );

// класс, хранящий обработчики-функторы
template<class ...TValues>
class MyHandlerContainer
{
    public:

        // конструктор; принимает переменное число разнотипных обработчиков-функторов
        MyHandlerContainer( const std::function<void( const TValues& )>... handlers );

        .....
        
        // статический метод создания экземрляра класса из обработчиков-методов
        template<class TMethodOwner>
        static MyHandlerContainer<TValues...>* createFrom( TMethodOwner& methodOwner, TMethodHandler<TMethodOwner, TValues>... handlers );

    .....
};

namespace
{
    // callback для проверки валидности обработчиков-методов
    template<class TMethodOwner>
    struct CheckCallback
    {
        // конструктор
        CheckCallback() :
            IsValid( true )
        {
        }
        
        // функция проверки каждого из обработчиков-методов
        template<std::size_t Index, class TValue>
        void operator()( const TMethodHandler<TMethodOwner, TValue>& oneMethodHandler )
        {
            if( oneMethodHandler == nullptr )
                IsValid = false;
        }
        
        bool IsValid;
    }

    // callback для создания набора обработчиков-функторов из набора обработчиков-методов
    template<class TMethodOwner>
    struct FunctorHandlerCallback
    {
        public:

            // конструктор
            FunctorHandlerCallback( TMethodOwner& methodOwner ) :
                m_methodOwner( methodOwner )
            {
            }

            // функция создания обработчика-функтора из обработчика-метода
            template<std::size_t Index, class TValue>
            std::function<void( const TValue& )> operator()( const TMethodHandler<TMethodOwner, TValue>& oneHandlers )
            {
                return [ this, oneHandlers ]( const TValue& tValue ) { ( m_methodOwner.*oneHandlers )( tValue ); };
            }

        private:

            TMethodOwner& m_methodOwner;
    };

    // callback для создания экземпляра класса 'MyHandlerContainer' из набора обработчиков-методов
    template<class ...TValues>
    struct CreateCallback
    {
        // функция создания экземпляра класса 'MyHandlerContainer' из набора обработчиков-методов
        auto operator()( std::function<void( const TValues& )>... handlers )
        {
            return new MyHandlerContainer<TValues...>( handlers... );
        }
    };
} //
template<class ...TValues>
template<class TMethodOwner>
MyHandlerContainer<TValues...>* MyHandlerContainer<TValues...>::createFrom( TMethodOwner& methodOwner, TMethodHandler<TMethodOwner, TValues>... handlers )
{
    // кортеж обработчиков-методов
    auto methodsTuple = std::make_tuple( handlers... );
    
    // проверим, все ли методы валидны
    CheckCallback checkCallback;
    tuple_utils::tupleForeach( checkCallback, methodsTuple );
    // если все методы валидны
    if( checkCallback.IsValid )
    {    
        // (нужно, чтобы он не удалился при выходе из функции)
        FunctorHandlerCallback<TMethodOwner>* functorHandlerCallback = new FunctorHandlerCallback<TMethodOwner>( methodHolder );
        // кортеж обработчиков-функторов
        auto handlersTuple = tuple_utils::mapTuple( *functorHandlerCallback, methodsTuple );
        // создание из кортеж обработчиков-функторов экземпляра класса 'MyHandlerContainer'
        MyHandlerContainer<TValues...>* result = tuple_utils::callTuple( CreateCallback<TValues...>( multiProperty ), handlersTuple );
        return result;
    }
    // если не все методы валидны
    assert( false );
    return nullptr;
}

Реализация без index_sequence

index_sequence появляется только в С++14. Если хочется использовать данные функции в С++11 (в котором и появился tuple), либо по каким-то иным причинам не хочется использовать index_sequence, либо просто интересно посмотреть на реализацию функций map и call без них, вот реализация:

Map

namespace tuple_utils
{
    // сформировать новый tuple (tuple другого типа) из результатов вызова 'callback'а для каждого элемента tuple'а
    /*
        struct callback
        {
            template<std::size_t, class R, class T>
            R operator()( T&& element )
            {
                // do something
            }
        };
        mapTuple( callback(), myTuple );
    */
    template<class TCallback, class TSourceTuple>
    auto mapTuple( TCallback& callback, const TSourceTuple& sourceTuple );

    namespace
    {
        template<std::size_t Index, class TCallback, class TSourceTuple, std::size_t... Indices>
        struct _map_
        {
            auto static mapTuple_( TCallback& callback, const TSourceTuple& sourceTuple )
            {
                const std::size_t idx = std::tuple_size<TSourceTuple>::value - Index;

                return _map_<Index - 1, TCallback, TSourceTuple, Indices..., idx>::mapTuple_( callback, sourceTuple );
            }
        };

        template<class TCallback, class TSourceTuple, std::size_t... Indices>
        struct _map_<0, TCallback, TSourceTuple, Indices...>
        {
            auto static mapTuple_( TCallback& callback, const TSourceTuple& sourceTuple )
            {
                return std::make_tuple( callback.operator()<Indices>( std::get<Indices>( sourceTuple ) )... );
            }
        };

    } //
    template<class TCallback, class TSourceTuple>
    auto mapTuple( TCallback& callback, const TSourceTuple& sourceTuple )
    {
        return _map_<std::tuple_size<TSourceTuple>::value, TCallback, TSourceTuple>::mapTuple_( callback, sourceTuple );
    }
} // tuple_utils

Call

namespace tuple_utils
{
    // вызвать 'callback', принимающий в качестве параметров распакованный tuple
    /*
        struct callback
        {
            template<class TResult, class ...TParams>
            TResult operator()( TParams... params )
            {
                // do something
            }
        };
        callTuple( callback(), myTuple );
    */
    template<class TCallback, class TResult, class ...TParams>
    TResult callTuple( TCallback& callback, const std::tuple<TParams...>& tuple );

    namespace
    {
        template<std::size_t Index, class TCallback, class TResult, class TTuple, class ...TParams>
        struct _call_
        {
            static TResult callTuple_( TCallback& callback, const TTuple& tuple, TParams... params )
            {
                const std::size_t idx = std::tuple_size<TTuple>::value - Index;

                return _call_<Index - 1, TCallback, TResult, TTuple, TParams..., typename std::tuple_element<idx, TTuple>::type>::callTuple_( callback, tuple, params..., std::get<idx>( tuple ) );
            }
        };

        template<class TCallback, class TResult, class TTuple, class ...TParams>
        struct _call_<0, TCallback, TResult, TTuple, TParams...>
        {
            static TResult callTuple_( TCallback& callback, const TTuple& tuple, TParams... params )
            {
                return callback( params... );
            }
        };

    } //
    template<class TCallback, class TResult, class ...TParams>
    TResult callTuple( TCallback& callback, const std::tuple<TParams...>& tuple )
    {
        return _call_<sizeof...( TParams ), TCallback, TResult, std::tuple<TParams...>>::callTuple_( callback, tuple );
    }
} // tuple_utils

Подход к реализации данных функций одинаков: мы вручную «накапливаем» индексы (вместо index_sequence) или параметры, а затем, в конце рекурсии, выполняем необходимые действия с уже полученным набором индексов/параметров. Хотя лично мне подход с индексами кажется более универсальным.

Спасибо, что уделили время!

Автор: AzrielFuzz

Источник

Поделиться

* - обязательные к заполнению поля