Search code examples
c++stlsfinae

How to implement a std::function with operator= that can check if its rhs has same signature


I'm learning to implement std::function and have found several articles about this. Unfortunately, none of their implementations can report a mistake when I assign a funtor with different signature. However, the STL version can alert me as I expect. Obviously, this kind of feature is useful to check such stupid mistake beforehand.

I read the source of MSVC implementation, but can not understand what happened within the template argument _Enable_If_callable_t of function& operator=(_Fx&& _Func) of std::function. Seems like this SFINAE trick prohibits such wrong assignment operator existing.

Please tell me the theory behind this and if I can implement such feature in my handmaking version.


Solution

  • Cppreference says about operator=:

    Sets the target of *this to the callable f, as if by executing function(std::forward(f)).swap(*this);. This operator does not participate in overload resolution unless f is Callable for argument types Args... and return type R.

    This can be easily checked with std::is_invocable_r helper:

    #include <type_traits>
    
    template <class>
    class function {};
    
    template <class R, class... Args>
    class function<R(Args...)> {
       public:
        template <typename F, typename x = std::enable_if_t<
                                  std::is_invocable_r<R, F, Args...>::value>>
        function& operator=(F&& f) {
            // logic...
            return *this;
        }
    };
    
    #include <functional>
    #include <string>
    
    void bar1(std::string) {}
    void bar2(int) {}
    void bar3(float) {}
    int bar4(int) { return 0; }
    
    int main(int argc, char** argv) {
        //std::function<void(int)> f;
        function<void(int)> f;
    
        // f = bar1; ERROR
        f = bar2;
        f = bar3;
        f = bar4;
        return 0;
    }
    

    I am using the SFINAE version with an extra template argument as I find it most clear but return-value-based can be used too. Same as for std::function, this is really forgiving - extra return values in case of R=void are ignored, implicit conversions are enabled. This matches what std::is_invocable_r allows.