Search code examples
c++c++17template-meta-programmingstdtupleparameter-pack

Extract types from std::tuple for a method signature


I am looking for a way to extract the types of an std::tuple to define a method signature. Take the following (contrived) example:

template <typename RetT, typename... ArgsT>
class A
{
public:
    typedef RetT ReturnType;
    typedef std::tuple<ArgsT...> ArgTypes;

    RetT doSomething(ArgsT... args)
    {
        // Doesn't make much sense, but it's just an example
        return (RetT) printf(args...);
    }
};

template <typename Enable, typename RetT, typename... ArgsT>
class AAdapter;

// Simply pass arguments along as-is
template <typename RetT, typename... ArgsT>
class AAdapter<std::enable_if_t<!std::is_same_v<RetT, float>>, RetT, ArgsT...> : public A<RetT, ArgsT...> {};

// Add additional first argument if RetT is float
template <typename RetT, typename... ArgsT>
class AAdapter<std::enable_if_t<std::is_same_v<RetT, float>>, RetT, ArgsT...> : public A<RetT, const char*, ArgsT...> {};



template <typename RetT, typename... ArgsT>
class B
{
public:
    typedef AAdapter<void, RetT, ArgsT...> AAdapter;

    // This needs to have the same method signature (return type and argument types) as AAdapter::doSomething()
    template <size_t... Index>
    typename AAdapter::ReturnType doSomething (
        typename std::tuple_element<Index, typename AAdapter::ArgTypes>::type... args
    ) {
        return a.doSomething(args...);
    }

public:
    AAdapter a;
};


int main(int argc, char** argv)
{
    // I would like to be able to remove the <0,1,2> and <0,1,2,3> below.
    B<int, const char*, int, int> b1;
    b1.doSomething<0,1,2>("Two values: %d, %d\n", 1, 2);

    B<float, const char*, int, int> b2;
    b2.doSomething<0,1,2,3>("Three values: %s, %d, %d\n", "a string", 1, 2);

    return 0;
}

Consider the way in which AAdapter changes, adds or removes argument types opaque. Basically, I want B::doSomething() to simply redirect to B::AAdapter::doSomething(), so I want both of these methods to have the exact same signature. The question is: How do I get the argument types of B::AAdapter::doSomething() from inside B?

My definition of B::doSomething() in the code above is the furthest I have come: I'm typedef'ing an std::tuple with the argument types inside A, so I can unpack them back to a parameter pack in B. Unfortunately, with the approach above I still need to provide the Index... template parameters manually when calling B::doSomething(). Surely there must be a way to have these Index... parameters automatically deduced from the size of the tuple. I have thought about approaches using std::make_integer_sequence, but that would require me to define an additional method argument for the sequence itself (and it can't be the last argument with a default value because no other arguments are allowed after a parameter pack).

Is there any way I can do this, with or without std::tuple? Solutions that require C++17 will be fine.

EDIT 1:

I realize now that I could probably circumvent the problem in my particular application by having B inherit from AAdapter instead of having an AAdapter object as a member, but I would still like to know how to solve the problem without having to do that.

EDIT 2:

Maybe some additional info on why AAdapter exists and what I want to achieve. I am implementing a kind of wrapper class around an existing C API that actually needs to be called in another process, RPC-style. So if the user wants to call a C function in the remote process, they will instead call a corresponding method in my wrapper class locally that handles all the RPC stuff like type conversions, the actual remote call and other ugly details. This wrapper class is represented by B in my code above. Now my wrapper method signature will usually not have the exact same signature as the C function. For example, the wrapper may have std::string_view instead of a pair of const char*, size_t that the C function has. For reasons that are not important here, it also needs to have an output parameter (a pointer) where the C function has a return value instead sometimes.

In order for me to not have to define two separate method signatures (in actuality it is three) and write code to convert the parameters for every single one, I instead pass only one of the signatures as template parameters RetT, ArgsT... to B. A signature conversion class (AAdapter in the example above) then applies rules for how to generate the second signature automatically from this first one by adding parameters, changing their types, etc.. A would then hold this generated signature, and B would have the one I provided initially. However, I want B to provide an invoke() method with the signature of A, thus hiding A and the entire method signature mess from the user completely. This is why I need access to the template parameter types of A from within B, and why I can't simply remove the middle class AAdapter.


Solution

  • The core of your problem is turning a tuple into an argument pack.

    maybe the tuple type is not the template arguments? in this case, there is a simple solution by inheritance:

    #include <vector>
    #include <iostream>
    #include <tuple>
    
    template<typename... Types>
    struct BImpl{
        typedef std::tuple<std::vector<Types>...> tuple_type;
        // maybe you will get a tuple type from some class templates. assume the 'tuple_type' is the result.
        // requirement: 'tuple_type' = std::tuple<SomeTypes...>
        // requirement: 'tuple_type' can be deduced definitely from template arguments 'Types...'.
        template<typename> // you can add other template arguments, even another std::tuple.
        struct OptCallHelper;
        template<typename... Args>
        struct OptCallHelper<std::tuple<Args...>>{
            auto dosomething(Args&&... args) /* const? noexcept? */{
                // do what you want...
                // requirement: you can definitely define the 'dosomething' here without any other informations.
                std::cout << "implement it here." << std::endl;
            }
        };
        typedef OptCallHelper<tuple_type> OptCall;
    };
    
    template<typename... Types>
    struct B : private BImpl<Types...>::OptCall{
        typedef typename BImpl<Types...>::OptCall base;
        using base::dosomething;
        // obviously, you can't change the implementation here.
        // in other words, the definition of 'dosomething' can only depend on template arguments 'Types...'.
    };
    
    
    int main(){
        B<int, float> b;
        b({}, {}); // shows "implement it here."
        return 0;
    }
    

    you can do what you want to do in BImpl and then use B instead.

       // This needs to have the same method signature (return type and argument types) as AAdapter::doSomething()
       template <size_t... Index>
       typename AAdapter::ReturnType doSomething (
           typename std::tuple_element<Index, typename AAdapter::ArgTypes>::type... args
       ) {
           return a.doSomething(args...);
       }
    

    for AAdaptor, I think you just want the interface of dosomething in A, and you can deduce it:

    #include <iostream>
    
    template<typename...>
    struct AAdaptor{
        int dosomething(){
            std::cout << "???" << std::endl;
            return 0;
        }
    };
    // ignore the implementation of AAdaptor and A.
    // just consider of how to get the interface of 'dosomething'.
    
    template<typename... Types>
    struct BImpl{
        typedef AAdaptor<Types...> value_type;
        typedef decltype(&value_type::dosomething) function_type;
        // attention: it won't work if 'AAdaptor::dosomething' is function template or overloaded.
        //            in this case, you should let A or AAdaptor give a lot of tuples to declare 'dosomething', referring to the first solution.
    
        
    
        template<typename>
        struct OptCallHelper;
        template<typename Ret, typename Klass, typename... Args>
        struct OptCallHelper<Ret(Klass::*)(Args...)>{
            value_type data;
            Ret dosomething(Args... args){
                return data.dosomething(args...);
            }
        };
        // attention: 'Ret(Klass::*)(Args...)' is different from 'Ret(Klass::*)(Args...) const', 'noexcept' as well in C++17.
        //            even Ret(Klass::*)(Args..., ...) is also different from them.
        //            you have to specialize all of them.
    
        typedef OptCallHelper<function_type> OptCall;
    };
    
    template<typename... Types>
    struct B : BImpl<Types...>::OptCall{
        typedef typename BImpl<Types...>::OptCall base;
        using base::dosomething;
    };
    
    
    int main(){
        B<int, float> b;
        b(); // shows "???"
        return 0;
    }
    

    if there is some difference between this code and your requirement, try to give another example to imply some of your implementation. it's still not clear what B gets and should do.