Reputation: 1766
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.
Upvotes: 1
Views: 62
Reputation: 172954
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.
Upvotes: 1
Reputation: 96569
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).
Upvotes: 2