2
\$\begingroup\$

I'm implementing a std::function like class that uses type erasure. So far it can be used with function objects (functions/function pointers, functors, lamdas) and pointer to member function.

But to support pointer to member function I used a C++17 feature, namely constexpr if, to choose at compile time which implementation to call.

This is the code:

#ifndef FUNCTION_H
#define FUNCTION_H

#include "move_forward.hpp"
#include "enable_if.hpp"
#include "traits.hpp"

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

/**** alias template for SFINAE ****/
template <typename Signature, typename T>
using IsNotSameAsFunction = traits::enable_if_t<!traits::is_same_v<Function<Signature>,traits::decay_t<T>>>;  // decayed type (used with fwd reference)

/**** swap function in global scope ****/
template <typename Signature>
void swap(Function<Signature> &a, Function<Signature> &b)
{
    a.Swap(b);
}

/**** partial class template specialization for function types ****/
template <typename ReturnType, typename... Args>
class Function<ReturnType(Args...)>
{
private:
    class CallableBase
    {
    public:
        virtual ~CallableBase() = default;
        virtual CallableBase *Clone() = 0;
        virtual ReturnType Invoke(Args... args) = 0;
    protected:
        CallableBase() = default;
    };
    template <typename T>
    class Callable : public CallableBase
    {
    public:
        template <typename U>
        Callable(U &&object);
        Callable *Clone() override;
        ReturnType Invoke(Args... args) override;
    private:
        template <typename U, typename... Args_>
        ReturnType Invoke_(U &&object, Args_... args);
        T mObject;
    };
public:
    // destructor
    ~Function() { delete mCallable; }

    // constructors
    Function(const Function &other);
    // Function(Function &other) : Function(static_cast<const Function&>(other)) {} // delegating constructor - if SFINAE is not used (inhibit forwarding constructor)
    Function(Function &&other);
    template <typename T, typename = IsNotSameAsFunction<ReturnType(Args...),T>>   /*typename = typename traits::enable_if<!traits::is_same<T,Function>::value>::type> */
    Function(T &&object);
    // assignment operators
    Function &operator=(const Function &other);
    // Function &operator=(Function &other) { return *this = static_cast<const Function&>(other); }  // if SFINAE is not used (inhibit forwarding assignment operator)
    Function &operator=(Function &&other);
    template <typename T, typename = IsNotSameAsFunction<ReturnType(Args...),T>>  /* typename = typename traits::enable_if<!traits::is_same<T,Function>::value>::type */
    Function &operator=(T &&object);

    // overloaded function call operator
    ReturnType operator()(Args... args) const { return mCallable->Invoke(args...); }

    // swap function
    void Swap(Function&);

    // conversion to bool
    explicit operator bool() { return mCallable != nullptr; }
private:
    CallableBase *mCallable;
};

/**** Callable class implementation ****/
template <typename ReturnType, typename... Args>
template <typename T>
template <typename U>
Function<ReturnType(Args...)>::Callable<T>::Callable(U &&object) : mObject(utility::forward<U>(object))
{
}

template <typename ReturnType, typename... Args>
template <typename T>
Function<ReturnType(Args...)>::Callable<T> *Function<ReturnType(Args...)>::Callable<T>::Clone()
{
    return new Callable(mObject);
}

template <typename ReturnType, typename... Args>
template <typename T>
ReturnType Function<ReturnType(Args...)>::Callable<T>::Invoke(Args... args)
{
    if constexpr (traits::is_pointer_to_memfun<traits::decay_t<T>>::value)
        return Invoke_(args...);
    else
        return mObject(args...);
}

template <typename ReturnType, typename... Args>
template <typename T>
template <typename U, typename... Args_>
ReturnType Function<ReturnType(Args...)>::Callable<T>::Invoke_(U &&object, Args_... args)
{
    return (object.*mObject)(args...);
}

/**** Function class implementation ****/
template <typename ReturnType, typename... Args>
Function<ReturnType(Args...)>::Function(const Function &other) : mCallable(other.mCallable->Clone())
{
}

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

template <typename ReturnType, typename... Args>
template <typename T, typename>
Function<ReturnType(Args...)>::Function(T &&object) : mCallable(new Callable<typename traits::decay<T>::type>(utility::forward<T>(object)))
{
}

template <typename ReturnType, typename... Args>
Function<ReturnType(Args...)> &Function<ReturnType(Args...)>::operator=(const Function &other)
{
    Function temp(other);
    Swap(temp);

    return *this;
}

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

    return *this;
}

template <typename ReturnType, typename... Args>
template <typename T, typename>
Function<ReturnType(Args...)> &Function<ReturnType(Args...)>::operator=(T &&object)
{
    Function temp(utility::forward<T>(object));
    Swap(temp);

    return *this;
}

template <typename ReturnType, typename... Args>
void Function<ReturnType(Args...)>::Swap(Function &other)
{
    CallableBase *temp = mCallable;
    mCallable = other.mCallable;
    other.mCallable = temp;
}

#endif // FUNCTION_H

in the traits namespace are trait classes that I wrote, they act as the std ones.

What I thought is I could call an overloaded function from Callable::Invoke, which resolves to the correct implementation. I would overload a private Callable::Invoke__ function, and use SFINAE or enable_if to choose at compile time.

But since my functions would have the same signature, the compiler complains that one of them cannot be overloaded. I hoped SFINAE kicks in before that but it doesn't seem the case.

How could I do it?

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Welcome to code review, where we review code that is working as expected to provide hints on how to improve that code. The question at the end of your question How could I do it? unfortunately indicates that the code is not working the way you want it to. I suggest you post this on SO first to get it working and then come back when it is to get a code review. \$\endgroup\$
    – pacmaninbw
    Commented Apr 18, 2020 at 22:21

1 Answer 1

2
\$\begingroup\$

What I thought is I could call an overloaded function from Callable::Invoke, which resolves to the correct implementation. I would overload a private Callable::Invoke__ function, and use SFINAE or enable_if to choose at compile time.

But since my functions would have the same signature, the compiler complains that one of them cannot be overloaded.

It's hard to tell what you're asking, because you're asking about code that you don't show (and you show code that you're not asking about). I infer that you want to take your current C++17 code and backport it to C++14?

template <typename ReturnType, typename... Args>
template <typename T>
ReturnType Function<ReturnType(Args...)>::Callable<T>::Invoke(Args... args)
{
    if constexpr (traits::is_pointer_to_memfun<traits::decay_t<T>>::value)
        return Invoke_(args...);
    else
        return mObject(args...);
}

What you're looking for is tag dispatch.

template<class F, class... Args>
auto Helper(std::true_type, F *self, Args... args) {
    return self->Invoke_(args...);
}

template<class F, class... Args>
auto Helper(std::false_type, F *self, Args... args) {
    return self->mObject(args...);
}

template <typename ReturnType, typename... Args>
template <typename T>
ReturnType Function<ReturnType(Args...)>::Callable<T>::Invoke(Args... args)
{
    return Helper(traits::is_pointer_to_memfun<traits::decay_t<T>>{}, this, args...)
}

If traits::is_pointer_to_memfun<traits::decay_t<T>> is a synonym for true_type (or derived from true_type), then the call will match Helper number one. If it's a synonym (or child) of false_type, then the call will match Helper number two.

See "A Soupçon of SFINAE" (me, CppCon 2017) for the details and more stuff like this.


Your out-of-line function template definitions make the code much harder to read than if you defined everything in-line, Java-style. More than one consecutive template<typename Foo> is too many.

Consider adding perfect forwarding on args... so you're not copying them by value all the time.

Consider replacing CallableBase *mCallable; with std::unique_ptr<CallableBase> mCallable; so that you don't have to do manual new and delete and so you can default your destructor, move-constructor, and move-assignment operator.

Your operator bool() should be marked const.

Your move-constructor should be marked noexcept.


By the way, another way to approach your problem would be to provide a partial specialization of Callable<T> specifically for things of the form Callable<R (C::*)(As...)>. Then you wouldn't need to distinguish different kinds of callables inside Callable<T>::Invoke; you'd have two completely different specializations of Callable<T>::Invoke, each handling only a single case.

\$\endgroup\$
6
  • \$\begingroup\$ how can I add support for perfect forwarding of arguments if Callable<T>::Invoke is a virtual function? (I cannot make that function a variadic template) \$\endgroup\$
    – Luca
    Commented Apr 21, 2020 at 8:39
  • 1
    \$\begingroup\$ Suppose Args... is <std::string, char&, double&&>. Then when you call Invoke(Args... args), it's equivalent to Invoke(std::string a, char& b, double&& c). But when you pass the args onward to self->mObject(args...), that's like self->mObject(a, b, c) — you've lost the fact that c should be passed as std::move(c) to preserve its rvalueness, and you've made an extra copy of a. Changing that line to forward them — self->mObject(std::forward<Args>(args)...) — will correctly preserve the value categories and avoid the extra copy (you'll just have a move instead). \$\endgroup\$ Commented Apr 21, 2020 at 14:07
  • \$\begingroup\$ isn't the Args... parameter pack always deduced as pass by value parameters? Shouldn't it be Args&&... if I want to pass by rvalue ref? I thought all parameters in the parameter pack would be deduced all as pass by value or all as pass by ref. \$\endgroup\$
    – Luca
    Commented Apr 21, 2020 at 15:59
  • 1
    \$\begingroup\$ @Luca: You are right about the rules for deduction, but there is no deduction happening here. When you instantiate Function<void(std::string, char&, double&&)>, you are explicitly specifying that Args... is <std::string, char&, double&&>. No deduction is needed; no deduction is possible. \$\endgroup\$ Commented Apr 22, 2020 at 15:37
  • \$\begingroup\$ Ok, I always use std::forward on universal/forwarding reference parameters or "local" auto&& forwarding reference (initialized from values returned from local function calls). I never considered forwarding an l-value that is a rvalue reference. It should move parameters that are rvalue references and pass-by value parameters and copy lvalue references, right? \$\endgroup\$
    – Luca
    Commented Apr 22, 2020 at 18:20

Not the answer you're looking for? Browse other questions tagged or ask your own question.