2

Consider a struct with static method templates that accept pointer-to-member functions. Note that when one of the arguments to the methods is an actual pointer-to-member function, both template parameters can be deduced, regardless if the other argument is a nullptr or not.

See questions below the following code:

struct Checker
{
    template <typename T, typename V>
    static void Check(
        V(T::*getter)(),
        void(T::*setter)(V)
    );

    template <typename T, typename V>
    static void CheckDefault(
        V(T::*getter)() = nullptr,
        void(T::*setter)(V) = nullptr
    );
};

struct Test
{
    int Value();
    void Value(int);

    int Getter();
    void Setter(int);
};

Checker::CheckDefault(&Test::Value);           //1
Checker::CheckDefault(&Test::Value, nullptr);  //2
Checker::Check(&Test::Value, nullptr);         //3

Checker::CheckDefault(&Test::Getter);          //4
Checker::CheckDefault(&Test::Getter, nullptr); //5
Checker::Check(&Test::Getter, nullptr);        //6
  • Why can the correct overload of &Test::Value be determined in 1, but not in 2 and 3?
  • Why are 1 and 4 able to deduce the correct typenames, but 2, 3, 5 and 6 not?

Edit

I was expecting to be able to call the methods with at least one of the two arguments set to an actual pointer-to-member function, causing deduction to succeed. Like so:

Checker::Check(&Test::Value, &Test::Value); // Both getter and setter
Checker::Check(&Test::Value, nullptr); // Only getter
Checker::Check(nullptr, &Test::Value); // Only setter

Edit

The discussion in the excepted answer by @Oliv explaining why it doesn't work as I expected, pointed me in the right direction for solving my specific problem.

I ended up using forwarders, as @Ben Voigt suggested. Something like:

template <typename T, typename V>
using GetterT = V(T::*)();

template <typename T, typename V>
using SetterT = void(T::*)(V);

template <typename T, typename V>
void Checker::CheckGetterAndSetter(
    GetterT<T, V> getter,
    SetterT<T, V> setter
)
{
    // Do stuff with getter and setter.
}

template <typename T, typename V>
void Checker::CheckGetter(
    GetterT<T, V> getter
)
{
    SetterT<T, V> null = nullptr;
    return CheckGetterAndSetter(getter, null);
}

template <typename T, typename V>
void Checker::CheckSetter(
    SetterT<T, V> setter
)
{
    GetterT<T, V> null = nullptr;
    return CheckGetterAndSetter(null, setter);
}
Community
  • 1
  • 1
jvdh
  • 309
  • 2
  • 11
  • Your question is more "why template argument deduction succeed when using the default argument and fails when not?" – Oliv Mar 02 '18 at 18:20

1 Answers1

1

TL;DR It succeeds because default arguments are not used for template argument deduction. In the others cases (2,3,5,6) it fails because nullptr_t has not the form of a T(U::*)(V) (not even a T*).

Let's take a simpler example:

template<class T>
void f(T,T*=nullptr);
int main(){
  f(10,nullptr);// (1) error
  f(10);// (2) OK
  }

For (1) the compiler considers it should be able to deduce T from both the first argument and second arguments because a parameter of the form T or T* are deductibles. Since nullptr_t as not the shape of a T* the compilation fails.

For (2) the compiler only performs template argument deduction for the first parameter of the function because default arguments does not play in template argument deduction. So it deduces without ambiguity that T is int. Then when the function will be called, nullptr will be converted to a int*.

Oliv
  • 17,610
  • 1
  • 29
  • 72
  • I now understand I was wrong in thinking that calling `Checker::CheckDefault(&Test::Value)` is a sort of "shorthand" and the compiler calls it as `Checker::CheckDefault(&Test::Value, nullptr)`, substituting the missing argument with its default value. – jvdh Mar 02 '18 at 18:51
  • Although this raises the question what a valid "empty" argument would be to a pointer-to-member function? – jvdh Mar 02 '18 at 18:54
  • @jvdh: If the first argument is always specified, and the second maybe not, you can simply remove the second from consideration during deduction. e.g. `template static void Check( V(T::*getter)(), identity_t::type setter );` – Ben Voigt Mar 02 '18 at 18:57
  • @jvdh What is proposed in this previous comment is to make the second argument always a *non deductible context*, that is a solution if you always provide a first "deductible from" argument. – Oliv Mar 02 '18 at 18:59
  • @Ben Voigt I left out the other examples (setting the first argument to `nullptr` and the second to a valid pointer-to-member function) for brevity. See updated question. – jvdh Mar 02 '18 at 19:03
  • @Oliv A I see what you mean. However, the signatures for getters and setters are different in this case, so I'm having trouble catching both in one template. – jvdh Mar 02 '18 at 19:04
  • 1
    @jvdh: Then use that one for when the first argument is specified, plus a forwarder for when it isn't `template static void Check( nullptr_t, void(T::*setter)(V) ) { V(T::*null_getter)() = nullptr; Check(null_getter, setter); }` – Ben Voigt Mar 02 '18 at 19:08