Universal reference in constructor causes failure, can't assign callable functor to std::function

681 views Asked by At

In the following complete test case, if I use the first ctor taking a functor by value and moving it into place then the code compiles and works as expected.

However if I use the universal reference ctor it fails to compile (I've included the clang error message below).

How can I fix this or what am I doing wrong?

#include <functional>
#include <utility>
#include <exception>

template<typename F>
struct py_catch {
    F func;
    /*  
    //Works
    py_catch(F f) 
    :   func ( std::move(f) )
    {   } */
    //Doesn't
    template<typename F2> 
    py_catch(F2&& f)  
    :   func ( std::forward<F2>(f) )
    {   }   

    py_catch(py_catch&&)=default;
    py_catch(const py_catch&)=default;
    py_catch& operator=(const py_catch&)=default;
    py_catch& operator=(py_catch&&)=default;

    template<typename... Args>
    auto operator()(Args&&... args)
    -> decltype(func(std::forward<Args>(args)...)) {
        try {
            return func(std::forward<Args>(args)...);
        }
        catch(const std::exception&) {
                    throw;
        }
    }   
};

template<typename F>
py_catch<typename std::remove_reference<F>::type> make_py_catch(F&& f) {
    return py_catch<typename std::remove_reference<F>::type>(std::forward<F>(f));
}

int main() {
    std::function<void()> s;
    s = make_py_catch([]{});
}

Compile error:

testcase2.cpp:16:7: error: no matching constructor for initialization of '<lambda at testcase2.cpp:43:23>'
        :   func ( std::forward<F2>(f) )
            ^      ~~~~~~~~~~~~~~~~~~~
/usr/lib/gcc/x86_64-unknown-linux-gnu/4.7.2/../../../../include/c++/4.7.2/functional:1764:10: note: in instantiation of function template specialization 'py_catch<<lambda at testcase2.cpp:43:23> >::py_catch<py_catch<<lambda at testcase2.cpp:43:23> > &>' requested here
            new _Functor(*__source._M_access<_Functor*>());
                ^
/usr/lib/gcc/x86_64-unknown-linux-gnu/4.7.2/../../../../include/c++/4.7.2/functional:1799:8: note: in instantiation of member function 'std::_Function_base::_Base_manager<py_catch<<lambda at testcase2.cpp:43:23> > >::_M_clone' requested here
              _M_clone(__dest, __source, _Local_storage());
              ^
/usr/lib/gcc/x86_64-unknown-linux-gnu/4.7.2/../../../../include/c++/4.7.2/functional:2298:33: note: in instantiation of member function 'std::_Function_base::_Base_manager<py_catch<<lambda at testcase2.cpp:43:23> > >::_M_manager' requested here
            _M_manager = &_My_handler::_M_manager;
                                       ^
/usr/lib/gcc/x86_64-unknown-linux-gnu/4.7.2/../../../../include/c++/4.7.2/functional:2173:4: note: in instantiation of function template specialization 'std::function<void ()>::function<py_catch<<lambda at testcase2.cpp:43:23> > >' requested here
          function(std::forward<_Functor>(__f)).swap(*this);
          ^
testcase2.cpp:43:7: note: in instantiation of function template specialization 'std::function<void ()>::operator=<py_catch<<lambda at testcase2.cpp:43:23> > >' requested here
    s = make_py_catch([]{});
      ^
testcase2.cpp:43:23: note: candidate constructor (the implicit copy constructor) not viable: no known conversion from 'py_catch<<lambda at testcase2.cpp:43:23> >' to 'const <lambda at testcase2.cpp:43:23>' for 1st argument
    s = make_py_catch([]{});
                      ^
testcase2.cpp:43:23: note: candidate constructor (the implicit move constructor) not viable: no known conversion from 'py_catch<<lambda at testcase2.cpp:43:23> >' to '<lambda at testcase2.cpp:43:23>' for 1st argument
    s = make_py_catch([]{});
                      ^
testcase2.cpp:43:23: note: candidate constructor (the implicit default constructor) not viable: requires 0 arguments, but 1 was provided
1 error generated.
2

There are 2 answers

3
dyp On BEST ANSWER

I think the problem is your converting constructor template template<typename F2> py_catch(F2&&) is too greedy. Another way to trigger the error is:

int main() {
    auto x( make_py_catch([]{}) );
    auto y(x);
}

This copy-construction uses an lvalue of some type py_catch<..>. The copy-ctor expects a py_catch<..> const&, whereas your greedy template provides an overload with a parameter of type py_catch<..>&. A special rule in [over.ics.rank]/3 now says the overload taking the reference-to-less-qualified-type is preferred. Therefore, not the copy-ctor, but the constructor template is called, which tries to initialize the data member (lambda) using the whole py_catch<..> object (instead of its func member).

A simple, but possibly not optimal solution is to provide another copy-ctor for non-const lvalues py_catch(py_catch&) = default;. But then, when you use inheritance or user-defined conversions, the constructor template will still be preferred.

Another solution is to use some SFINAE on the constructor template; for example check for is_same, is_base_of or something similar (remember to remove_reference the possible reference from F2). is_convertible might work as well, but I suspect it would be recursively trying to use the constructor template to do its check.

2
111111 On

Okay, I found a better fix, which hints at the actually problem.

I suspected the issue lay in the copy and move ctors for some reason choosing the template rather than the defaulted ctors. This meant that py_catch<lambda_types?> being passed to the template ctor and being forwarded to func.

So I added a test in the ctor using SFINAE and this fixed the issue as it will reject anything other than matching func types.

Like so:

template     
<    
    typename F2,    
    typename =typename std::enable_if<std::is_same<F2, F>::value, F>::type    
>    
py_catch(F2&& f)    
:   func ( std::forward<F2>(f) )    
{   }

Ugly yes.

I have to wait a few days before I can mark this correct, so if someone can tell me why it's not choosing the defaulted ctors over the template then I'll mark that correct.