简体   繁体   中英

Method pointer template doesn't compile

The following C++ code does not compile in MS Visual Studio 2010:

class Foo
{
public:
    /// Provides the signature of the methods that can be given to addValueSetListener
    template <typename TT>
    struct ChangeHandler
    {
        typedef void ( TT::* OnSetValueMethod )();
    };

    template <typename TT>
    void bar_ok(TT*, void ( TT::* )(), bool = false) {}
    template <typename TT>
    void bar_ok(const char*, TT*, void ( TT::* )()) {}

    template <typename TT>
    void bar_fails(TT*, typename ChangeHandler<TT>::OnSetValueMethod, bool = false) {}
    template <typename TT>
    void bar_fails(const char*, TT*, typename ChangeHandler<TT>::OnSetValueMethod) {}

    void testBar() {}
};

int main()
{
  Foo foo; 
  foo.bar_ok   ("allo",& foo, & Foo::testBar);  // compiles
  foo.bar_fails("allo",& foo, & Foo::testBar);  // compile ERROR
}

The compiler error is 'TT': must be a class or namespace when followed by '::' , for the ERROR line.

The only difference between the line that fails and the one that doesn't is that bar_fails declares the "method pointer type" argument void (TT::*)() via a "templated typedef", whereas bar_ok declares it directly.

Note that without the overload for const char* , the templated typedef works fine. With the const char* overload available, the compiler erroneously chooses the TT=[const char] overload of bar_fails yet it correctly chooses the TT=Foo overload of bar_ok. This issue does not arise when the typedef is for a "simple" data like TT* or float*.

The reason is that in the bar_ok case, SFINAE can apply, as the erroneous construct const char::* appears in direct context of template parameter substitution. In the bar_fail case, it's one step removed (hidden in the "template typedef"), which means SFINAE no longer applies and the compiler must process the syntactic nonsense of const char::* , thus halting and reporting the error.

In other words, it's not that the compiler chooses the wrong overload in bar_fail . It has to examine both overloads in both cases, but in the first one is it allowed to disregard the erroneous one by SFINAE, while in the second one it's "too late."

Angew provided a simple explanation of why the code in my OP doesn't work. So is there a workaround? Turns out it is really simple, but the explanation doesn't fit in a comment, so here it is.

As pointed out by Angew, SFINAE only applies at the direct substitution level, so this compiles:

template <typename TT> void testFunc(TT) {}
template <typename TT> void testFunc(typename TT::Foo) {}
int main()
{
    testFunc<int>(3);
}

Indeed the compiler knows to drop the second overload of testFunc : an integer doesn't have a nested Foo type. If SFINAE weren't available at all in C++, this would halt compilation.

Now if you change the above implementation slightly to use a traits-like class, the following completely equivalent code no longer compiles:

template <typename TT>
struct Helper
{
    typedef typename TT::Foo MyFoo;
};
template <typename TT> void testFunc(TT) {}
template <typename TT> void testFunc(typename Helper<TT>::MyFoo) {}

int main()
{
    testFunc<int>(3);
}

because the compiler is "inside" the Helper<int> class when it is resolving the MyFoo typedef; it balks at the int::Foo , direct SFINAE does not apply, so it gives up compilation.

You might notice that in my OP, I didn't specify the template parameter explicitly, whereas in the above, I do: that's because the compiler knows param is an int so it matches all testFunc that take int as parameter, it doesn't try all testFunc<int> . In my OP, I didn't need to specify the template parameter explicitly to get the error, because the first function argument did this for me. Let's do away with the method pointer because it clouds the problem, my original problem is exhibited by the following simpler code where I don't explicitly specify the template parameter:

struct Foo
{
    template <typename TT> struct Helper
    {
        typedef typename TT::Foo MyFoo;
    };

    template <typename TT> void bar(TT*, typename Helper<TT>::MyFoo) {}
    template <typename TT> void bar(const char*, TT*) {}
};


int main()
{
    Foo foo; 
    foo.bar("allo", & foo);  // ok
}

The compiler sees the first function call parameter to foo.bar as a const char* , so it knows it has to consider both bar(const char*, Foo*) and bar(const char*, Helper<const char>::MyFoo) . Then inside Helper<const char> it tries to resolve const char::Foo and halts compilation (again direct SFINAE does not apply).

One solution is to drop the Helper and stick to direct nested type:

struct Foo
{
    template <typename TT> void bar(TT*, typename TT::Foo) {}
    template <typename TT> void bar(const char*, TT*) {}
};

However this is not ideal for me because in my actual code my bar() is expecting a method pointer and I'm trying to make this clear in the declaration (though I'm wondering now if the templated typedef helps or hinders, but that's another matter).

The second solution is to use SFINAE to make compiler balk at the bar level. Turns out this is easy to do: define a specialization of Helper for const char that is missing parts:

struct Foo
{
    template <typename TT> struct Helper
    {
        typedef typename TT::Foo MyFoo;
    };
    template <> struct Helper<const char>
    {
    };

    template <typename TT> void bar(TT*, typename Helper<TT>::MyFoo) {}
    template <typename TT> void bar(const char*, TT*) {}
};

Now when the compiler matches TT to const char , it has two overloads to consider:

void bar(const char*, Helper<const char>::MyFoo)
void bar(const char*, Foo*)

But Helper<const char>::MyFoo doesn't exist in the specialization, so SFINAE can be used: the compiler hasn't had to go "inside" Helper<T> .

Those 3 lines for specialization were sufficient to solve the problem.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM