Search code examples
c++c++14variadic-templates

Variadic template with type checks


I have some opencl kernel wrapper objects which take arguments, currently I run them like this:

kernel.setArg<0>(count);
kernel.setArg<1>(data);

But it would be a lot cleaner if I could run it like this:

kernel.setArgs(count, data);

I want this type safe, since the arguments can only be of either int or buffer&.

My first thought was variadic templates and I got it working, but without type safety:

#include <iostream>

struct buffer {};
class kernel {
    template<int arg, int numArg, typename T>
    void setArgs_(T& data) {
        setKernelArg<numArg>(data);
    }
    template<int arg, int numArg, typename T, typename... Args>
    void setArgs_(T& data, Args&&... params) {
        setKernelArg<arg>(data);
        setArgs_<arg+1, numArg, Args...>(params...);
    }
    template<int argNum>
    void setKernelArg(buffer& data) {
        std::cout <<"setting arg " << argNum << " to buffer" << std::endl;
        //set arg argNum to data, long function
    }
    template<int argNum>
    void setKernelArg(int data) {
        std::cout <<"setting arg " << argNum << " to int" << std::endl;
        //set arg argNum to data, long function
    }
public:
    template<typename T, typename... Args>
    void setArgs(T& data, Args&&... params) {
        setArgs_<0, sizeof...(params), T, Args...>(data, params...);
    }
};

int main(int argc, char** argv) {
    buffer a,b,c;
    int i,j,k;
    kernel kern;
    kern.setArgs(a,i,b,j,c,k);
    return 0;
}

The only types that should be supported, at compile time, are int and buffer&, but I can't get it working like I want. I tried std::enable_if:

template<typename T, typename... Args>
typename std::enable_if<std::is_same<T, buffer&>::value> setArgs(T& data, Args&&... params) {
    setArgs_<0, sizeof...(params), buffer&, Args...>(data, params...);
}
template<typename T, typename... Args>
typename std::enable_if<std::is_same<T, int>::value> setArgs(T& data, Args&&... params) {
    setArgs_<0, sizeof...(params), int, Args...>(data, params...);
}

but then I get

varargs_typesafe.cpp:41:29: error: call of overloaded ‘setArgs(buffer&, int&, buffer&, int&, buffer&, int&)’ is ambiguous
 kern.setArgs(a,i,b,j,c,k);
                         ^
varargs_typesafe.cpp:28:62: note: candidate: std::enable_if<std::is_same<T, buffer&>::value> kernel::setArgs(T&, Args&& ...) [with T = buffer; Args = {int&, buffer&, int&, buffer&, int&}]
     typename std::enable_if<std::is_same<T, buffer&>::value> setArgs(T& data, Args&&... params) {
                                                              ^
varargs_typesafe.cpp:32:58: note: candidate: std::enable_if<std::is_same<T, int>::value> kernel::setArgs(T&, Args&& ...) [with T = buffer; Args = {int&, buffer&, int&, buffer&, int&}]
     typename std::enable_if<std::is_same<T, int>::value> setArgs(T& data, Args&&... params) {

Feels like my std::enable_if isn't working properly or I'm missing something.

EDIT: I tried this by giving my working code giving it a bool as argument and it works, but a std::string fails. Is this because a bool can be converted to an int? Can I disallow such casts somehow?


Solution

  • You were almost there. You can use this if you want to accept only buffer& and int:

    template<typename T, typename... Args>
    std::enable_if_t<std::is_same<T, buffer>::value>
    setArgs(T &data, Args&&... params) {
        setArgs_<0, sizeof...(params), T, Args...>(data, std::forward<Args>(params)...);
    }
    
    template<typename T, typename... Args>
    std::enable_if_t<std::is_same<T, int>::value>
    setArgs(T data, Args&&... params) {
        setArgs_<0, sizeof...(params), T, Args...>(data, std::forward<Args>(params)...);
    }
    

    Or this if you plan to accept only lvalue references for both the types:

    template<typename T, typename... Args>
    std::enable_if_t<std::is_same<T, buffer>::value or std::is_same<T, int>::value>
    setArgs(T& data, Args&&... params) {
        setArgs_<0, sizeof...(params), T, Args...>(data, std::forward<Args>(params)...);
    }
    

    Or even this if you want to accept any kind of reference for both the types:

    template<typename T, typename... Args>
    std::enable_if_t<std::is_same<std::decay_t<T>, buffer>::value or std::is_same<std::decay_t<T>, int>::value>
    setArgs(T&& data, Args&&... params) {
        setArgs_<0, sizeof...(params), T, Args...>(std::forward<T>(data), std::forward<Args>(params)...);
    }
    

    In this case I would use forwarding references and std::forward through the rest of your class too, especially for the arguments.