Peregrin
Peregrin

Reputation: 436

restrict function pointer member template to only derived classes

I'm trying to limit template deduction only to objects from the same hierarchy.

The code below compiles

template<class T>
void RegisterAction(const string& actionId, bool(T::*f)())
{
    m_actions.emplace(actionId, std::bind(f, static_cast<T*>(this)));
}

but this code doesn't

template<class T, typename std::enable_if_t<std::is_base_of<BaseClass, T>::value>>
void RegisterAction(const string& actionId, bool(T::*f)())
{
    m_actions.emplace(actionId, std::bind(f, static_cast<T*>(this)));
}

m_actions is of type std::unordered_map<string, std::function<bool()>>

Here is the error from Visual Studio

'BaseClass::RegisterAction': no matching overloaded function found
error C2783: 'void BaseClass::RegisterAction(const string &,bool (__cdecl T::* )(void))': could not deduce template argument for '__formal'

This is how you would use the method:

void DerivedClass::InitActions()
{
    RegisterAction("general.copy", &DerivedClass::OnCopy);
    RegisterAction("general.paste", &DerivedClass::OnPaste);
}

Btw, I can't use static_assert because there I'm using c++14.

Doesn't anyone has any idea?

Upvotes: 1

Views: 116

Answers (2)

Pavlo Mur
Pavlo Mur

Reputation: 347

When it is intended to use RegisterAction to register only for those classes that are derived from BaseClass, is seems that it is better to state with static_assert explicitly why some T cannot be used with RegisterAction instead of just "hiding" the problem with SFINAE.

So

template<class T>
void RegisterAction(const string& actionId, bool(T::*f)())
{
    static_assert(
        std::is_base_of<BaseClass, T>::value,
        "T shall be derived from BaseClass for RegisterAction to work"
    );
    m_actions.emplace(actionId, std::bind(f, static_cast<T*>(this)));
}

will scream loudly and clearly about exact reason of why RegisterAction cannot accept some actions.

Upvotes: 0

Brian Bi
Brian Bi

Reputation: 119219

You're trying to introduce a new template parameter in order to cause a substitution error---which is correct---but your syntax is slightly incorrect. What you should write is:

template<class T, typename = std::enable_if_t<std::is_base_of<BaseClass, T>::value>>
                       // ^^^ this equal sign is crucial

When you write typename = foo, you're declaring an unnamed type template parameter (it's like writing typename unused = foo) and making the default value for that type foo. Thus, if someone tries to instantiate this template with T not derived from BaseClass, a substitution failure occurs in the default argument, causing deduction to fail.

Since you wrote it without the equal sign, typename std::enable_if_t<...> was interpreted as a typename-specifier, that is, the compiler thinks you're declaring a non-type template parameter whose type is typename std::enable_if_t<...>, which you have left unnamed. Consequently, when T is derived from BaseClass, the type of this template parameter is void. Since non-type template parameters cannot have type void (as there are no values of type void), a SFINAE error occurs here.

Interestingly, both GCC and Clang also fail to give a useful error message. They also complain that the unnamed template argument cannot be deduced, rather than pointing out that void non-type template parameters are invalid (or even just pointing out that it is a non-type template parameter of type void).

Upvotes: 2

Related Questions