c++templatesstdtuple

Functions chaining with std::tuple


I'm trying to implement functions chain, that get bunch of different functions and member functions at compile time and execute them one by one and stop execution if processing function fails. It may be any count of functions. Now I'm stuck on the fact that std::tuple::get doesn't accept counted variable as template parameter. How can I implement something like this?

#include <tuple>
#include <stdlib.h>

template<class...Fs>
class Functions
{
public:
    Functions(Fs...functions)
        : m_functions(functions...)
    {
    }

    template<class Arg>
    void operator()(Arg&& arg)
    {
        CallAll<sizeof...(Fs)>(std::forward<Arg>(arg));
    }

private:
    std::tuple<Fs...> m_functions;

    template<size_t index, class Arg>
    decltype(auto) CallSingle(Arg&& arg)
    {
        return std::get<index>(m_functions)(arg);
    }


    template<size_t index, class Arg>
    void CallAll(Arg&& arg)
    {
        while (index != 0) {
            auto ret = CallSingle<index - 1>(std::forward<Arg>(arg));
            if (std::is_same<decltype(ret), bool>()) {
                if (!ret)
                    return;
            }
            index--; // error!
        }
    }
};

int main(int argc, char *argv[])
{
    Functions f{
        [](int x) { return x + 1; },
        [](int x) { return false; },
        [](int x) { return x + 3; }
    };

    f(42);
    return 0;
}

Compile error:

../../main.cpp: In member function 'void Functions<Fs>::CallAll(Arg&&)':
../../main.cpp:90:18: error: decrement of read-only location 'index'
   90 |             index--;
      |                  ^~
../../main.cpp:90:18: error: lvalue required as decrement operand

Solution

  • The comment by @user12002570 points out the reason why your approach fails:

    NTTPs are compile time constants(constexpr). So you can't modify index like in index--

    However, you don't actually need to do it if you have C++17. Instead, you can make use of the fold expressions and get rid of your current auxiliary function callAll(). You also need to modify the signature of callOne(). Your operator() can be as follows.

        template <typename F, typename Arg>
        static bool callOne(F&& function, Arg&& arg){
            auto doCall=[&](){
                return std::forward<F>(function)(std::forward<Arg>(arg));
            };
            
            if constexpr(std::is_same_v<decltype(doCall()), bool>){
                return doCall();
            }
            else{
                doCall();
                return true;
            }
        }
    
        template<class Arg>
        bool operator()(Arg&& arg)
        {
            return std::apply([&arg](auto&&... functions){
                return (callOne(std::forward<Fs>(functions),std::forward<Arg>(arg)) && ...);
            }, m_functions);
        }
    

    which uses a lambda to capture the arg and std::apply to unpack the tuple for you. The fold expression then collects the results and automatically provides the short-circuiting behavior you are looking for.

    Compared to the answer by @MarekR, this answer allows passing function pointers and other callables directly as arguments without first wrapping them in lambdas. This may be slightly simpler from the users' perspective.