Search code examples
templatesoverloadingc++98method-signaturecallable-object

How can I overload a function with a callable object as a parameter based on the object's call signature?


For example, given the following code

class A {
 public:
    double operator()(double foo) {
        return foo;
    }
};

class B {
 public:
    double operator()(double foo, int bar) {
        return foo + bar;
    }
};

I want to write two versions of fun, one that works with objects with A's signature and another one that works with objects with B's signature:

template <typename F, typename T>
T fun(F f, T t) {
    return f(t);
}

template <typename F, typename T>
T fun(F f, T t) {
    return f(t, 2);
}

And I expect this behavior

A a();
B b();
fun(a, 4.0);  // I want this to be 4.0
fun(b, 4.0);  // I want this to be 6.0

Of course the previous example throws a template redefinition error at compile time.

If B is a function instead, I can rewrite fun to be something like this:

template <typename T>
T fun(T (f)(T, int), T t) {
    return f(t, 2);
}

But I want fun to work with both, functions and callable objects. Using std::bind or std::function maybe would solve the problem, but I'm using C++98 and those were introduced in C++11.


Solution

  • Here's a solution modified from this question to accommodate void-returning functions. The solution is simply to use sizeof(possibly-void-expression, 1).

    #include <cstdlib>
    #include <iostream>
    
    // like std::declval in c++11
    template <typename T>
    T& decl_val();
    
    // just use the type and ignore the value. 
    template <std::size_t, typename T = void> 
    struct ignore_value {typedef T type;};
    
    // This is basic expression-based SFINAE.
    // If the expression inside sizeof() is invalid, substitution fails.
    // The expression, when valid, is always of type int, 
    // thanks to the comma operator.
    // The expression is valid if an F is callable with specified parameters. 
    template <class F>
    typename ignore_value<sizeof(decl_val<F>()(1),1), void>::type
    call(F f)
    {
        f(1);
    }
    
    // Same, with different parameters passed to an F.
    template <class F>
    typename ignore_value<sizeof(decl_val<F>()(1,1),1), void>::type
    call(F f)
    {
        f(1, 2);
    }
    
    void func1(int) { std::cout << "func1\n"; }
    void func2(int,int) { std::cout << "func2\n"; }
    
    struct A
    {
        void operator()(int){ std::cout << "A\n"; }
    };
    
    struct B
    {
        void operator()(int, int){ std::cout << "B\n"; }
    };
    
    struct C
    {
        void operator()(int){ std::cout << "C1\n"; }
        void operator()(int, int){ std::cout << "C2\n"; }
    };
    
    int main()
    {
        call(func1);
        call(func2);
        call(A());
        call(B());
        // call(C()); // ambiguous
    }
    

    Checked with gcc and clang in c++98 mode.