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

时间:2016-09-28 21:58:21

标签: c++ templates gcc clang 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 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; }

1 个答案:

答案 0 :(得分:2)

这很有意思! 我认为它是一个g ++编译器错误,我认为这就是发生的事情。我用g ++ 4.9.3和clang 3.7.0尝试了几次代码修改。

虽然功能与类模板实例化有一些不同的规则,但我相信这些是模板实例化的一般步骤:

  1. 编译器使用模板定义读取源文件。
  2. 名称查找(可能触发ADL):这是一个名称的过程, 在程序中遇到时,与声明相关联 介绍它。 (http://en.cppreference.com/w/cpp/language/lookup
  3. 模板参数规范/演绎:为了 实例化一个函数模板,每个模板参数必须是 已知但不是必须指定每个模板参数。什么时候 可能,编译器将推断出缺少的模板参数 来自函数参数。 (http://en.cppreference.com/w/cpp/language/template_argument_deduction
  4. 模板替换(可触发SFINAE):a的每次使用 函数参数列表中的模板参数替换为 相应的模板参数。替换失败(即, 未能使用推导或提供的模板参数替换模板参数 函数模板的模板参数)删除了该函数 来自过载集的模板。 (http://en.cppreference.com/w/cpp/language/function_template#Template_argument_substitution
  5. 形成过载集:在过载解决开始之前, 通过名称查找和模板参数推导选择的函数 被组合以形成候选函数集。 (http://en.cppreference.com/w/cpp/language/overload_resolution#Details
  6. 重载分辨率:一般来说,候选函数是哪个 参数最接近的参数匹配是 调用。 (http://en.cppreference.com/w/cpp/language/overload_resolution
  7. 模板实例化:必须确定模板参数 这样编译器就可以生成一个实际的函数(或类,来自 一个类模板)。 (http://en.cppreference.com/w/cpp/language/function_template#Function_template_instantiation
  8. 编译器生成代码。
  9. 我将这些要点作为指导和参考,以便日后使用。此外,我将参考步骤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) - &gt; f())的类型为 void ,并且从下面的示例中, a c 都是由专业而不是第一个模板调用的。原因是因为第二个模板比第一个模板更专业,因此是两者最佳匹配(一石二鸟)(模板形式订购规则:https://stackoverflow.com/a/9993549/2754510)。来自 decltype(&amp; T :: f)的类型是 M4GolfFvvE (可能的翻译:Men 4 Golf Fear非常邪恶的Elk),感谢boost :: typeindex :: type_id_with_cvr,它被解构为 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(&amp; T :: f)):

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

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

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