Template function does not work for pointer-to-member-function taking const ref

Viewed 407

Lately I wrote a template function to solve some code repetitions. It looks like this:

template<class T, class R, class... Args>
R call_or_throw(const std::weak_ptr<T>& ptr, const std::string& error, R (T::*fun)(Args...), Args... args) {
    if (auto sp = ptr.lock()) 
    {
        return std::invoke(fun, *sp, args...);
    }
    else 
    {
        throw std::runtime_error(error.c_str());
    }
}

int main() {
    auto a = std::make_shared<A>();
    call_or_throw(std::weak_ptr<A>(a), "err", &A::foo, 1);
}

This code works perfectly well for class A which looks like this:

class A {
public:
    void foo(int x) {

    }
};

But fails to compile for one like this:

class A {
public:
    void foo(const int& x) {

    }
};

Why is it so (by why I mean why it fails to deduce the type) and how (if it is possible at all) can I make this code work with references? Live example

3 Answers

Args types cannot be deduced both as const& (from fun parameter declaration) and non-reference from args declaration. A simple fix is to use two separate template type parameter packs:

template<class T, class R, class... Args, class... DeclaredArgs>
R call_or_throw(
    const std::weak_ptr<T>& ptr,
    const std::string& error,
    R (T::*fun)(DeclaredArgs...),
    Args... args);

As a downside, I can imagine slightly longer error messages in case of bad usage.

Note that the template parameter Args's type is deduced as const int& on the 3rd function argument &A::foo, and deduced as int on the 4th function parameter 1. They don't match and cause deduction fails.

You can exclude the 4th parameter from deduction, e.g.

template<class T, class R, class... Args>
R call_or_throw(const std::weak_ptr<T>& ptr, 
                const std::string& error, 
                R (T::*fun)(Args...), 
                std::type_identity_t<Args>... args) {
//              ^^^^^^^^^^^^^^^^^^^^^^^^^^                

LIVE

PS: std::type_identity is supported since C++20; but it's quite easy to implement one.

Your issue is that you have conflict deductions for Args between:

  • R (T::*fun)(Args...)
  • Args... args

I suggest to have more generic code (no duplications between R (T::*fun)(Args...) and
const version R (T::*fun)(Args...) const and other alternative) with:

template<class T, class F, class... Args>
decltype(auto) call_or_throw(const std::weak_ptr<T>& ptr,
                             const std::string& error,
                             F f,
                             Args&&... args)
{
    if (auto sp = ptr.lock()) 
    {
        return std::invoke(f, *sp, std::forward<Args>(args)...);
    }
    else 
    {
        throw std::runtime_error(error.c_str());
    }
}
Related