Private member access in template substitution and SFINAE

橙三吉。 提交于 2021-02-08 12:19:11

问题


class A { int a; };

template<typename, typename = void>
class test {};

template<typename T>
class test<T,decltype(T::a)> {};

int main() { test<A> a; }

The code above compiles without error on clang version 3.8.0-2ubuntu4 (tags/RELEASE_380/final), but fails to compile on g++-5 (Ubuntu 5.4.1-2ubuntu1~16.04) 5.4.1 20160904 and g++-6 (Ubuntu 6.2.0-3ubuntu11~16.04) 6.2.0 20160901 with errors like this:

main.cpp: In function ‘int main()’:
main.cpp:9:22: error: ‘int A::a’ is private within this context
 int main() { test<A> a; }
                      ^
main.cpp:1:15: note: declared private here
 class A { int a; };

In both cases I compiled with -std=c++11, but the effect is the same for -std=c++14 and -std=c++1z.

Which compiler is correct here? I assumed that, at least since C++11, accessing private members during template substitution should trigger SFINAE, implying that clang is correct and gcc is not. Is there some additional rule I am unaware of?

For referencce, I am thinking of the note in §14.8.2/8 of the current standard draft N4606:

If a substitution results in an invalid type or expression, type deduction fails. An invalid type or expression is one that would be ill-formed, with a diagnostic required, if written using the substituted arguments. [ Note: If no diagnostic is required, the program is still ill-formed. Access checking is done as part of the substitution process. — end note ]

Using a member function and a member function pointer instead is accepted by both compilers:

class A { void a() {}; };

template<typename, typename = void>
class test {};

template<typename T>
class test<T,decltype(&T::a)> {};

int main() { test<A> a; }

回答1:


This is very interesting! I think it's a g++ compiler bug and I think that this is what happens. I've tried several alterations of your code with g++ 4.9.3 and clang 3.7.0 .

Although there are somewhat different rules to function vs class template instantiation, I believe that these are the general steps to a template instantiation:

  1. Compiler reads source file with template definitions.
  2. Name lookup (may trigger ADL) : It is the procedure by which a name, when encountered in a program, is associated with the declaration that introduced it. (http://en.cppreference.com/w/cpp/language/lookup)
  3. Template argument specification / deduction : In order to instantiate a function template, every template argument must be known, but not every template argument has to be specified. When possible, the compiler will deduce the missing template arguments from the function arguments. (http://en.cppreference.com/w/cpp/language/template_argument_deduction)
  4. Template substitution (may trigger SFINAE) : Every uses of a template parameter in the function parameter list is replaced with the corresponding template arguments.Substitution failure (that is, failure to replace template parameters with the deduced or provided template arguments) of a function template removes the function template from the overload set. (http://en.cppreference.com/w/cpp/language/function_template#Template_argument_substitution)
  5. Forming the overload set: Before overload resolution begins, the functions selected by name lookup and template argument deduction are combined to form the set of candidate functions. (http://en.cppreference.com/w/cpp/language/overload_resolution#Details)
  6. Overload resolution : In general, the candidate function whose parameters match the arguments most closely is the one that is called. (http://en.cppreference.com/w/cpp/language/overload_resolution)
  7. Template Instantiation : the template arguments must be determined so that the compiler can generate an actual function (or class, from a class template). (http://en.cppreference.com/w/cpp/language/function_template#Function_template_instantiation)
  8. Compiler generates code.

I'll keep these bullet-points as guidelines and references for later on. Furthermore, I'll refer to template evaluation from steps 1-6. If you find anything wrong in the above list please feel free to change it or comment so that I can make the changes.

In the following example:

class A {};

template<typename, typename = void>
struct test
{ test(){std::cout<< "Using None" <<std::endl;} };

template<typename T>
struct test<T, decltype(T::a)>
{ test(){std::cout<< "Using T::a" <<std::endl;} };

int main()
{ test<A> a; }

Output from both compilers:

Using None

This example compiles fine in both g++ and clang, because, when the compiler completes the evaluation process of all the templates, it will only choose to instantiate the first template for being the best match to the template arguments used to create the object in main(). Also, the template substitution process fails when the compiler fails to deduce T::a (SFINAE). Furthermore, due to the argument mismatch, the specialization will not be included in the overload set and will not be instantiated.

Should we add the second template argument, like this:

test<A , decltype(A::a)> a;

The code will not compile and both compilers would complain of:

error: no member named 'a' in 'A'

In the following example however, things start becoming weird:

class A { int a; };

template<typename, typename = void>
struct test
{ test(){std::cout<< "Using None" <<std::endl;} };

template<typename T>
struct test<T, decltype(T::a)>
{ test(){std::cout<< "Using T::a" <<std::endl;} };

int main()
{ test<A> a; }

Output from clang:

Using None

Output from g++:

error: ‘int A::a’ is private

To begin with, I think that this would have been a nice warning. But why an error? The template won't even get instantiated. Considering the previous example, and the fact that pointers-to-members are constant values known at compile time, it seems, that when clang completes the template evaluation stage, with the SFINAE occuring at template substitution, it accurately instantiates the first template and ignores the specialization. But when g++ goes through the substitution process, and looks for the variable T::a, it sees that it is a private member, and instead of saying SFINAE, it prompts with the error above. I think that this is where the bug is, considering this bug report: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=61806

Now, the curious part is in the next example, which uses the private member function:

class A{ void a() {}; };

template<typename, typename = void>
struct test
{ test(){std::cout<< "Using None" <<std::endl;} };

template<typename T>
struct test<T,decltype(&T::a)>
{ test(){std::cout<< "Using A::a" <<std::endl;} };

int main()
{ test<A> a; }

Output by both compilers:

Using None

If the previous explanation is true, then why doesn't g++ prompt an error when the private member function is used? Again, this is only an assumption based on the outputs, but I think that this bit actually works as it should. Long story short, SFINAE kicks in, the specialization is discarded from the overload set, and only the first template gets instantiated. Maybe there's more to it than meets the eye, but if we explicitly specify the second template argument, both compilers will prompt the same error.

int main()
{ test<A , decltype(&A::a)> b; }

Output by both compilers:

error: ‘void A::a()’ is private

Anyway, this is the final code I've been using. To demonstrate the outputs, I've made the class public. As an interesting event, I've added a nullptr to point to the member function directly. The type from decltype(((T*)nullptr)->f()) is void, and from the example below, a and c are both invoked by the specialization rather than the first template. The reason is because the second template is more specialized than the first and therefore is the best match for both of them (kill two birds with one stone) (Template Formal Ordering Rules : https://stackoverflow.com/a/9993549/2754510). The type from decltype(&T::f) is M4GolfFvvE (possible translation: Men 4 Golf Fear very vicious Elk), which thanks to boost::typeindex::type_id_with_cvr, it is demangled into void (Golf::*)().

#include <iostream>
#include <boost/type_index.hpp>

class Golf
{
    public:
        int v;

        void f()
        {};
};


template<typename T>
using var_t = decltype(T::v);

template<typename T>
using func_t = decltype(&T::f);
//using func_t = decltype(((T*)nullptr)->f()); //void


template<typename, typename = void>
struct test
{
    test(){std::cout<< "Using None" <<std::endl;}
};

template<typename T>
struct test<T,var_t<T> >
{
    test(){std::cout<< "Using Golf::v" <<std::endl;}
};

template<typename T>
struct test<T,func_t<T> >
{
    test(){std::cout<< "Using Golf::f" <<std::endl;}
};


int main()
{
    test<Golf> a;
    test<Golf,var_t<Golf> > b;
    test<Golf,func_t<Golf> > c;

    using boost::typeindex::type_id_with_cvr;
    std::cout<< typeid(func_t<Golf>).name() << " -> " << type_id_with_cvr<func_t<Golf>>().pretty_name() <<std::endl;
}

Output from both compilers (func_t = decltype(&T::f)):

Using None
Using Golf::v
Using Golf::f
M4GolfFvvE -> void (Golf::*)()

Output from both compilers (func_t = decltype(((T*)nullptr)->f())):

Using Golf::f
Using Golf::v
Using Golf::f
v -> void

Edit:

According to @Dr.Gut (comment below) this bug continues to exist in gcc 9.2. However I found a "hack" around this using std::declval which makes it look even weirder.

#include <utility>

class A
{
    int a;
};

template<typename, typename = void>
class test
{};

template<typename T>
class test<T,decltype(std::declval<A>().a)>
{};

int main()
{
    test<A> a;
}

Online example: https://rextester.com/BUFU29474

The code compiles and runs fine in g++ and vc++ but fails in clang++.



来源:https://stackoverflow.com/questions/39758335/private-member-access-in-template-substitution-and-sfinae

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!