Constexpr on member function pointer value - undefined behavior?

Viewed 305

I have a problem with a simple reflection like mechanism in C++. I would like to have one template type, which should behave differently with different member function pointers as template arguments:

[Solution #1, by standard it is bad]

If I have a class template with a class type and its member function pointer, I cannot specialize partially to the member pointer being null, because I cannot specialize "non-type template argument with dependent type" (see: https://en.cppreference.com/w/cpp/language/partial_specialization argument list [5])

template<class O, void(O::*foo)() = nullptr>
struct p
{};

template<class O, void(O::*foo)()>
struct p<O, nullptr>
{};

[Solution #2, GCC problem]

If I try to specialize to a deduced constexpr value, which reflects to the member pointer, I have different problems with the compilers. GCC8.2 x64 tells me that the nullptr == foo expression is not constant in the context: p<A, &A::f> j;. However ARM GCC8 says it is okay. I think it is some memory layout problem, that struct A is not complete at the time the template engine tries to evaluate a concrete function pointer.

template<class O, void(O::*foo)() = nullptr, bool = nullptr == foo>
struct p
{};

template<class O, void(O::*foo)()>
struct p<O, foo, true>
{};

struct A
{
    void f();
    p<A, &A::f> j;
};

[Solution #2 usage, MSVC problem]

It is strange that MSVC 19.5 x86 is okay with the above, it has another problem. It is working, while struct A is standalone, or it inherits from one struct. But when there are two base structs (X, Y), it dies with internal compiler error.

template<class O, void(O::*foo)() = nullptr, bool = nullptr == foo>
struct p
{};

template<class O, void(O::*foo)()>
struct p<O, foo, true>
{};

struct X{};
struct Y{};

struct A : X, Y
{
    void f();
    p<A> i;
    p<A, &A::f> j;
};

Clang7 works with all the above well.

Can anyone explain, if this is an undefined behavior, or is there any guide about this topic in the standard?

2 Answers

This is CWG 2127. Basically, we have this restriction in [temp.class.spec]:

The type of a template parameter corresponding to a specialized non-type argument shall not be dependent on a parameter of the specialization.

Which basically means that we can't directly do the kind of nullptr specialization you want to do. Which is... lame. But any problem in programming can be solved by adding another layer of indirection right?

template<class O, void(O::*foo)(), bool is_null>
struct p_impl { /* non-null case */ };

template<class O, void(O::*foo)()>
struct p_impl<O, foo, true> { /* null case */ };

template<class O, void(O::*foo)() = nullptr>
using p = p_impl<O, foo, foo == nullptr>;

gcc on the other hand just seems to have a problem comparing a pointer to member of incomplete type. It rejects this:

template <bool> struct Z { };

struct C
{
    void f();
    Z<&C::f == nullptr> z; // not a constant expression
};

This is gcc bug 56428, I think.

The problem here is that you're comparing a function pointer (that is not constant, as it may be known only at linking time) to nullptr (that is).

You can solve this by using std::integral_constant to generate the relative constant for such function type, and then you can compare it.

So something like (for basic C++11) is:

template<class O>
using F = void(O::*)();

template<class O>
using NullF = std::integral_constant<F<O>, nullptr>;

template<class O, F<O> foo = nullptr,
    bool = std::is_same<std::integral_constant<F<O>, foo>, NullF<O>>::value>
struct p
{};

template<class O, F<O> foo>
struct p<O, foo, true>
{};

struct A
{
    void f();
    p<A, &A::f> j;
    p<A> k;
};

With C++17 you can maybe just use a function with if constexpr checks:

template<class O>
using F = void(O::*)();

template<class O>
using NullF = std::integral_constant<F<O>, nullptr>;

template<class O, F<O> foo = nullptr>
struct p
{
    p()
    {
        if constexpr (std::is_same_v<std::integral_constant<F<O>, foo>, NullF<O>>)
        {
            // yay
        }
    }
};

struct A
{
    void f();
    p<A, &A::f> j;
    p<A> i;
};
Related