模板替换中的私人成员访问和SFINAE

class A { int a; };

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

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

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

上面的代码在编译clang version 3.8.0-2ubuntu4 (tags/RELEASE_380/final)编译没有错误,但是无法在g++-5 (Ubuntu 5.4.1-2ubuntu1~16.04) 5.4.1 20160904上编译g++-5 (Ubuntu 5.4.1-2ubuntu1~16.04) 5.4.1 20160904g++-6 (Ubuntu 6.2.0-3ubuntu11~16.04) 6.2.0 20160901错误如下:

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; };

在这两种情况下,我使用-std=c++11编译,但对于-std=c++14-std=c++1z ,效果相同。

哪个编译器在这里正确? 我认为,至少从C ++ 11开始,在模板替换过程中访问私有成员应该触发SFINAE,这意味着clang是正确的,而gcc不是。 有没有一些额外的规则我不知道?

对于参考,我正在考虑现行标准草案N4606的§14.8.2/ 8中的注释:

如果替换导致无效的类型或表达式,则键入演绎失败。 无效的类型或表达式是不合格的,如果使用替代参数编写,则需要诊断。 [注意:如果不需要诊断,程序仍然不合格。 访问检查是替代过程的一部分。 - 结束注释]

两个编译器都接受使用成员函数和成员函数指针:

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

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

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

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

这非常有趣! 我认为这是一个g ++编译器错误, 我认为这是发生了什么。 我已经用g ++ 4.9.3和clang 3.7.0试了几次你的代码的改动。

尽管函数vs类模板实例化有一些不同的规则,但我相信这些是模板实例化的一般步骤:

  • 编译器使用模板定义读取源文件。
  • 名称查找(可能触发ADL):程序中遇到的名称与引入它的声明相关联的过程。 (http://en.cppreference.com/w/cpp/language/lookup)
  • 模板参数说明/演绎:为了实例化函数模板,每个模板参数必须是已知的,但不是每个模板参数都必须指定。 如果可能,编译器会从函数参数中推导出缺少的模板参数。 (http://en.cppreference.com/w/cpp/language/template_argument_deduction)
  • 模板替换(可能触发SFINAE):函数参数列表中模板参数的每次使用均被替换为相应的模板参数。函数模板的替换失败(即,未能用模拟参数替换模板参数)从过载集中删除函数模板。 (http://en.cppreference.com/w/cpp/language/function_template#Template_argument_substitution)
  • 形成重载集合:在重载解析开始之前,将通过名称查找和模板参数推导选择的函数组合起来形成候选函数集合。 (http://en.cppreference.com/w/cpp/language/overload_resolution#Details)
  • 重载分辨率:一般来说,其参数与参数最接近匹配的候选函数是被调用的函数。 (http://en.cppreference.com/w/cpp/language/overload_resolution)
  • 模板实例化:必须确定模板参数,以便编译器可以生成实际功能(或类模板中的类)。 (http://en.cppreference.com/w/cpp/language/function_template#Function_template_instantiation)
  • 编译器生成代码。
  • 以后我会将这些要点保留为指导原则和参考。 此外,我将参考步骤1-6中的模板评估。 如果您在上面的列表中发现任何错误, 随时更改或评论,以便我可以进行更改。

    在以下示例中:

    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; }
    

    两个编译器的输出:

    Using None
    

    这个例子在g ++和clang中编译都很好,因为当编译器完成所有模板的评估过程时,它只会选择实例化第一个模板,以便与用于在main()中创建对象的模板参数最佳匹配)。 另外,当编译器未能推导出T :: a(SFINAE)时,模板替换过程失败。 此外,由于参数不匹配,专业化将不会包含在重载集中,并且不会被实例化。

    我们是否应该添加第二个模板参数,如下所示:

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

    代码不会编译,两个编译器都会抱怨:

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

    然而,在下面的例子中,事情变得很奇怪:

    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; }
    

    clang输出:

    Using None
    

    g ++的输出:

    error: ‘int A::a’ is private
    

    首先,我认为这将是一个很好的警告。 但为什么会出现错误? 该模板甚至不会被实例化。 考虑到前面的例子以及指向成员的指针是编译时已知的常量值这一事实,看起来,当clang完成模板评估阶段时,SFINAE发生在模板替换时,它准确地实例化第一个模板并忽略专业化。 但是,当g ++经过替换过程并查找变量T :: a时,它看到它是一个私有成员,而不是说SFINAE,它会提示上面的错误。 我认为这是错误所在,考虑到这个错误报告:https://gcc.gnu.org/bugzilla/show_bug.cgi?id = 61806

    现在,好奇的部分在下一个示例中,它使用私有成员函数:

    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; }
    

    两个编译器的输出:

    Using None
    

    如果前面的解释是真的,那么为什么g ++在使用私有成员函数时不会提示错误? 再次,这只是基于输出的假设,但我认为这一点实际上是应该的。 长话短说,SFINAE开始了,专业化从重载集合中被丢弃,只有第一个模板被实例化。 也许除此之外还有更多,但如果我们明确指定第二个模板参数,那么两个编译器都会提示相同的错误。

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

    两个编译器的输出:

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

    无论如何,这是我一直在使用的最终代码。 为了展示输出结果,我公开了这个课程。 作为一个有趣的事件,我添加了一个nullptr直接指向成员函数。 来自decltype(((T *)nullptr) - > f())的类型无效的 ,并且从下面的例子中,a和c都被特化而不是第一个模板调用。 原因是因为第二个模板比第一个模板更专业,因此是两个模板的最佳匹配(一石二鸟)(模板正式订购规则:https://stackoverflow.com/a/9993549/2754510 )。 来自decltype(&T :: f)的类型M4GolfFvvE (可能的翻译是:Men 4 Golf Fear非常恶毒的Elk),这要归功于boost :: typeindex :: type_id_with_cvr,它被demangled 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;
    }
    

    两个编译器的输出(func_t = decltype(&T :: f)):

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

    两个编译器的输出(func_t = decltype(((T *)nullptr) - > f())):

    Using Golf::f
    Using Golf::v
    Using Golf::f
    v -> void
    
    链接地址: http://www.djcxy.com/p/58715.html

    上一篇: Private member access in template substitution and SFINAE

    下一篇: What does a compiler check for uninstantiated template code?