简体   繁体   中英

templates and overload resolution

I'm reading the book "C++ templates, complete guide", and on chapter 22 it introduces the concept of type erasure with an example of a std::function-linke class:

#include "functorwrapper.hpp"

// primary template (declaration)
template <typename Signature>
class Function;

// partial class template specialization
template <typename ReturnType, typename... Args>
class Function<ReturnType(Args...)>
{
public:
    // constructors
    Function() : mFunctorWrapper(nullptr) {}                 // default constructor
    Function(const Function &);                              // copy constructor
    Function(Function &&);                                   // move constructor
    template <typename Functor> Function(Functor &&);        // generalized constructor

    // destructor
    ~Function() { delete mFunctorWrapper; }

    // copy/move operations
    Function &operator=(Function const &);                          // copy assignmaent operator
    Function &operator=(Function &&);                               // move assignment operator 
    template <typename Functor> Function &operator=(Functor &&);    // generalized assignment operator

    // overloaded function call operator
    ReturnType operator()(Args...);
private:
    FunctorWrapperBase<ReturnType(Args...)> *mFunctorWrapper;
};

template <typename ReturnType, typename... Args>
Function<ReturnType(Args...)>::Function(const Function &other) : mFunctorWrapper(nullptr)
{
    if (other.mFunctorWrapper)
        mFunctorWrapper = other.mFunctorWrapper->Clone();
}

template <typename ReturnType, typename... Args>
Function<ReturnType(Args...)>::Function(Function &&other) :  mFunctorWrapper(other.mFunctorWrapper)
{
    other.mFunctorWrapper = nullptr;
}

template <typename ReturnType, typename... Args>
template <typename Functor>
Function<ReturnType(Args...)>::Function(Functor &&functor)
{
    // remove reference if l-value (template type argument deduced as Functor &)
    mFunctorWrapper = new FunctorWrapper<typename std::remove_reference<Functor>::type, ReturnType(Args...)>(std::forward<Functor>(functor));
}

template <typename ReturnType, typename... Args>
Function<ReturnType(Args...)> &Function<ReturnType(Args...)>::operator=(const Function &other)
{
    mFunctorWrapper = other.mFunctorWrapper->Clone();

    return *this;
}

template <typename ReturnType, typename... Args>
Function<ReturnType(Args...)> &Function<ReturnType(Args...)>::operator=(Function &&other)
{
    mFunctorWrapper = other.mFunctorWrapper;
    other.mFunctorWrapper = nullptr;

    return *this;
}

template <typename ReturnType, typename... Args>
template <typename Functor>
Function<ReturnType(Args...)> &Function<ReturnType(Args...)>::operator=(Functor &&functor)
{
    mFunctorWrapper = new FunctorWrapper<typename std::remove_reference<Functor>::type, ReturnType(Args...)>(std::forward<Functor>(functor));
}

template <typename ReturnType, typename... Args>
ReturnType Function<ReturnType(Args...)>::operator()(Args... args)
{
    mFunctorWrapper->Invoke(args...);
}

this class just manages the memory allocated for an object of type FunctorWrapper, which is a class template that represents different kinds of functors (or callables).

If I construct an object of type Function from a function object, a lambda or a pointer to a function it all goes well (I can call the object and the relative function is called).

But if I try to copy construct (or move construct) a Function from another Function, the compiler binds the call only to the constructor that takes an arbitrary object (the generalized constructor with template parameter Functor and an universal reference as function parameter), causing a crash.

I thought that if I call the constructor like:

Function<void(double)> fp4(&FreeFunction);
fp4(1.2);

Function<void(double)> fp5 = fp4;  // copy construction

the copy constructor should be called, since it's more specialized. I followed the example on the book, but I must be doing something wrong.

I think it's a defect in the book.

the copy constructor should be called, since it's more specialized

template <typename Functor> Function(Functor &&); is a better match.

After typename Functor is deduced as Function<...> & , the constructor turns into Function(Function &); , which is a better match than Function(const Function &); if you pass a non-const object to it.

You can fix this with SFINAE:

template
<
    typename Functor,
    typename = std::enable_if_t<!std::is_same_v<Function,
        std::remove_cv_t<std::remove_reference_t<Functor>>>>
>
Function(Functor &&);

You need to do the same thing with the assignment operator. Alternatively, you could simply remove it (assigning a functor should still work, as the compiler should call Function(Functor &&) followed by the move assignment).

Yes, unfortunately the forwarding reference version is a better match than the copy constructor, which requires an implicit conversion to the reference to const .

You can put constraints as

template <typename Functor, std::enable_if_t<!std::is_base_of_v<Function, std::decay_t<Functor>>>* = nullptr> 
Function(Functor &&);  

PS: I used std::is_base_of instead of std::is_same , for the case that Function might be inherited, and in the implementation of copy constructor of the derived class, the copy constructor of Function might be invoked with an argument with the derived class type.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM