Search code examples
c++templatesc++17variadic-templatestemplate-meta-programming

Variadic template class implicitly converting


I have a class and need to validate that it's function calls are being called w/ the right parameters. The function signature is always the same (sans 1 argument type). So, naturally I went for a templated approach. So generally the validation policy would have a template parameter per data type it could handle:

using P = Policy<int, double, UserDefined>

Or something of that ilk. I got it to compile, but the caveat is that if double and int (or anything a double can convert to actually) are both template parameters, the double will be implicitly converted.

The policy looks like this:

template <typename... T>
class BasicValidationPolicy { };

template <>
class BasicValidationPolicy<>
{
public:
    void RegisterSetHandler();
};

template <typename T, typename... Rest>
class BasicValidationPolicy<T, Rest...> : public BasicValidationPolicy<Rest...>
{
public:
    using SetHandler = std::function<void(int, T)>;
    
    void RegisterSetHandler(const SetHandler& handler)
    {
        m_setHandler = handler;
    }
    
    void Set(int n, const T& val) {
        if (m_setHandler) {
            m_setHandler(n, val);
        }
    }
    
private:
    SetHandler m_setHandler{nullptr};
};

The class that uses it...

template <typename ValidatorPolicy>
class MyClass : public ValidatorPolicy  {
public:

    void OnSetInt(int n, int64_t v)
    {
        ValidatorPolicy::Set(n, v);
    }
    
    void OnSetDouble(int n, double d)
    {
        ValidatorPolicy::Set(n, d);
    }
};

Usage:

int main()
{
    using Policy = BasicValidationPolicy<int64_t, double>; // doesn't work
    MyClass<Policy> m;
    
    m.Policy::RegisterSetHandler([](int i, double value) {
        // by this point value is an int64_t
        std::cout << "Got double " << i << ", " << value << "\n";
    });
   
    double d{35.2135}; 
    m.OnSetDouble(1, d);
}

To boot, doing this does work

using Policy = BasicValidationPolicy<double, int64_t>;

So I guess I'm missing something about the template deduction. Looks like it tries to match double against std::int64_t says "meh, good enough", and moves on. Nice to know a way around it (kind of) but that looks like it would be very tricky to maintain.


Solution

  • It's complicated...

    First of all: you have a recursive template class, BasicValidationPolicy, where you define two methods and you want that all methods, for all recursion steps of the class, are available.

    Unfortunately, the definition of the methods in the derived classes hide the method in base classes.

    To un-hide the inherited methods, you have to explicitly add a pair of using

    using BasicValidationPolicy<Rest...>::Set;
    using BasicValidationPolicy<Rest...>::RegisterSetHandler;
    

    At this point, the code doesn't compile because you need a Set() and a RegisterSetHandler() in the ground case class. You have declared a dummy RegisterSetHandler() but not a dummy Set(). You have to add one, so the ground case become

    template <>
    class BasicValidationPolicy<>
    {
    public:
        void RegisterSetHandler();
        void Set();
    };
    

    Now your MyClass<Policy> object expose two RegisterSetHandler() methods (before only one): one receiving a std::function<void(int, std::int64_t)>, the other (before hidden) receiving a std::function<void(int, double)>.

    But when you pass a lambda, you have a chicken-and-egg problem: the lambda can be converted to a std::function but isn't a std::function. So can't be used to deduce the template parameters of std::function because the types are to be known before to deduce them.

    A possible solution is impose a lambda/std::function conversion in the call

    // ..........................VVVVVVVVVVVVVV
    m.Policy::RegisterSetHandler(std::function{[](int i, double value) {
                                 // by this point value is an int64_t
                                 std::cout << "Got double " << i << ", " << value << "\n";
                                 }});
    // ...........................^
    

    using also the template deduction guides introduced in C++17.

    So your code become

    #include <iostream>
    #include <functional>
    
    template <typename... T>
    class BasicValidationPolicy { };
    
    template <>
    class BasicValidationPolicy<>
    {
    public:
        void RegisterSetHandler();
        void Set();
    };
    
    template <typename T, typename... Rest>
    class BasicValidationPolicy<T, Rest...> : public BasicValidationPolicy<Rest...>
    {
    public:
        using SetHandler = std::function<void(int, T)>;
    
        using BasicValidationPolicy<Rest...>::Set;
        using BasicValidationPolicy<Rest...>::RegisterSetHandler;
        
        void RegisterSetHandler(const SetHandler& handler)
        {
            m_setHandler = handler;
        }
        
        void Set(int n, const T& val) {
            if (m_setHandler) {
                m_setHandler(n, val);
            }
        }
        
    private:
        SetHandler m_setHandler{nullptr};
    };
    
    template <typename ValidatorPolicy>
    class MyClass : public ValidatorPolicy  {
    public:
    
        void OnSetInt(int n, int64_t v)
        {
            ValidatorPolicy::Set(n, v);
        }
        
        void OnSetDouble(int n, double d)
        {
            ValidatorPolicy::Set(n, d);
        }
    };
    
    
    int main ()
     {
       using Policy = BasicValidationPolicy<int64_t, double>; // doesn't work
       MyClass<Policy> m;
        
       m.Policy::RegisterSetHandler(std::function{[](int i, double value) {
                                    std::cout << "Got double " << i << ", " << value << "\n";
                                    }});
       
        double d{35.2135}; 
        m.OnSetDouble(1, d);
    }