V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
scinart
V2EX  ›  C

求助,问一个 c++模板推导的问题。

  •  
  •   scinart · 2017-09-03 22:19:43 +08:00 · 2635 次点击
    这是一个创建于 2622 天前的主题,其中的信息可能已经有所发展或是发生改变。

    我想知道一个类里有没有定义 value_type,但是为什么 has_value_type_1 可以做到,has_value_type_2 做不到。查了半天了,还是没搞明白。

    // c++98 version
    #include <iostream>
    
    struct false_type { const static bool value=false; };
    struct true_type  { const static bool value=true;  };
    
    template <typename> struct type_sink { typedef void type; };
    template <typename, typename = void> struct has_value_type_1 : false_type {};
    template <typename, typename = void> struct has_value_type_2 : false_type {};
    
    template <typename T> struct has_value_type_1< T, typename type_sink< typename T::value_type >::type > : true_type {};
    template <typename T> struct has_value_type_2< T, typename T::value_type > : true_type {};
    
    struct A { typedef int value_type; };
    struct B { };
    
    template <typename T>
    bool f_1(T) { return has_value_type_1<T>::value; }
    
    template <typename T>
    bool f_2(T) { return has_value_type_2<T>::value; }
    
    #include <iostream>
    int main()
    {
        A a;
        B b;
        bool x[4] = {f_1(a), f_1(b), f_2(a), f_2(b)};
        for(int i=0;i<4;i++)
            std::cout<<x[i]<<' ';
        return 0;
    }
    
    12 条回复    2017-09-04 07:51:56 +08:00
    yorTX9t
        1
    yorTX9t  
       2017-09-03 22:22:51 +08:00
    你用的是什么编译器? VC 在 two phase name look up 的实现上有 bug,你用的是这个么?还没看代码,只是猜测。
    gnaggnoyil
        2
    gnaggnoyil  
       2017-09-03 22:45:13 +08:00
    has_value_type2 的偏特化错了.has_value_type<A, void>并不能选取 has_value_type<A, int>作为自己的特化实例.
    scinart
        3
    scinart  
    OP
       2017-09-03 23:02:28 +08:00
    @yorTX9t 我 clang 4.0 和 gcc 7.1

    @gnaggnoyil 我想偏特化,但是编译器没给我偏特化,所以是不是编译器应该给我报个错?但是我这没错没警告,那 clang 和 gcc 背后干了啥?
    yorTX9t
        4
    yorTX9t  
       2017-09-03 23:39:25 +08:00
    ```
    //template <typename, typename = void> struct has_value_type_2 : false_type {};
    template <typename, typename = int> struct has_value_type_2 : false_type {};
    ```
    gnaggnoyil
        5
    gnaggnoyil  
       2017-09-03 23:47:35 +08:00   ❤️ 1
    @scinart 因为你 has_value_type2 的写法就弄出来了一个不偏特化也是 well-formed 的上下文啊……具体原因我 2L 已经说了,has_value_type2<A>因为是 has_value_type2<A, void>,而 void != typename A::value_type,所以是用的主模板版本进行的实例化.
    scinart
        6
    scinart  
    OP
       2017-09-04 00:02:36 +08:00
    @gnaggnoyil 我能理解它用主模板版本进行的实例化,我不理解的是,当编译器看到 has_value_type_2< A, int> 的时候,它是怎么做的。

    我的理解是:has_value_type_2 需要两个模板参数,第二个不写则默认为 void

    template <typename T> struct has_value_type_2 是一个 partial specialization,两个模板参数分别是 T, typename T::value_type

    那么,从模板匹配上说,has_value_type<A, int>成功匹配上了 partial specialization 的模板,为什么还要使用主模板呢?
    gnaggnoyil
        7
    gnaggnoyil  
       2017-09-04 00:12:40 +08:00
    @scinart 因为试图对偏特化模板进行匹配的实例不是 has_value_type2<A, int>,而是 has_value_type2<A, void>……你注意到主模板第二个参数的默认参数是什么了吗……
    yorTX9t
        8
    yorTX9t  
       2017-09-04 00:12:55 +08:00
    @gnaggnoyil 我来说下我的理解吧。不一定正确,只供参考。

    1. 在 template definition phase,也就是 two-phase name lookup 的 phase 1,编译器在处理 has_value_type_1<T>::value 和 has_value_type_2<T>::value 的时候,编译器寻找 non-dependent 的模板匹配,这时候 has_value_type_1<T> 和 has_value_type_2<T> 都被理解为从 false_type 里边继承的,因为 template <typename, typename = void> struct has_value_type_1 : false_type {} 不需要依赖任何模板参数。于是函数 template< typename T> bool f_1(T) { return has_value_type_1<T>::value; } 被理解为 template<typename T> bool f_1(T){ return has_value_type_1<T,void>::value; }; ; f_2(T) 也是如此,被理解为 has_value_type_2<T,void>::value;,因为编译器不能得知模板参数 T 的内部,因此后边的两个偏特化 template <typename T> struct has_value_type_1< T, typename type_sink< typename T::value_type >::type > : true_type {}; 和 template <typename T> struct has_value_type_2< T, typename T::value_type > : true_type {}; 都不会被处理。

    2. 在 template instantiation phase,也就是 phase 2,编译器为 has_value_type_1<T,void> 寻找合适的匹配,这时候编译器看到了两个偏特化,一个写出来是 template< typename T = A > struct has_value_type_1<A,void>:true_type;,另一个写出来是 template< typename T = A > struct has_value_type_2<A,int>:true_type;。于是头一个被选中,替代 bool f_1(A) 中的那个 has_value_type_1<A,void>,于是为 true_type,而另外一个不变,还是 false type。

    如果将主楼代码中的 template <typename> struct type_sink { typedef void type; }; 替换为 template <typename> struct type_sink { typedef int type; }; 那么所有的输出当为 false,因为偏特化时 has_value_type_1 也未被选中。
    yorTX9t
        9
    yorTX9t  
       2017-09-04 00:54:55 +08:00
    @scinart 啊,上边的回复 at 错了人
    yangff
        10
    yangff  
       2017-09-04 01:22:29 +08:00   ❤️ 1
    因为你用模板的姿势有点偏差…… 首先你可能误解了模板的特化…… 模板的特化实际上是模式匹配,而不是某种自动填充……
    你可以试试 template <class T> struct X {}; template<> struct X<int> {};在做的是当 X=int 的时候选择后一条路径做特化,而不是当我不填 T 的时候选择 T=int 来编译后面那个特化……你的 has_value_type_2 很明显是在干这件事…… 如果你想这么干,请用继承或者 typedef 之类的…… 看你的要求

    其次你可能误解了模板的默认值

    模板的默认值就是默认值,只要你不填这个参数他就永远是默认值……

    然后你这里的问题是…… 你之所以能写 has_value_type_<T>实际上是因为你的 typename=void, 这个默认值和你怎么特化无关,也就是说也就是去掉这个=void,把你的模板参数完整写出来,你在写的其实一直都是 has_value_type_1<T, void> has_value_type_2<T, void>,你完全可以搞个 struct has_value_type_XX : has_value_type_1<T, void>来代替这个 typename=void,这样会显得更清晰一些……

    然后我们来看你的 has_value_type_1
    template <typename T> struct has_value_type_1< T, typename type_sink< typename T::value_type >::type > : true_type {};

    不难注意到,如果 T 有 value_type,无论他的类型是什么,你始终通过 type_sink 对 has_value_type_1<T, void>做特化,使得它为 true
    反之,如果 T 没有 value_type,由于 SFINAE,就不会有这条特化,从而去匹配 primary 也就是 struct has_value_type_1<T, typename = void> : false_type 这条路径,于是就可以得到正确结果

    然后我们来看你的第二个写法,struct has_value_type_2<T, typename T::type_value>,那么 T::type_value 是啥? 是 int,也就是说你特化了 has_value_type_2<T, int> : true_value ;而你调用的是啥? has_value_type_2<T, void>,于是这里并不匹配,c++用的还是 has_value_type_2 : false_value 这条。从而你取到的还是 false,除非你显示使用 has_value_type_2<T, int>或者把 A 的 int 改成 void …… 这两者明显是不大靠谱的……
    scinart
        11
    scinart  
    OP
       2017-09-04 02:42:21 +08:00
    @yangff @yorTX9t 感谢回复,这下懂了。

    然后再次感谢一下 @yangff 解释的太清楚了。

    总结一下我的理解误差:我以为 template <typename T> struct has_value_type 是定义了一个只接受一个模板参数的特殊的 has_value_type,当编译器运到 has_value_type 只有一个模板参数时,会优先选择这个定义:

    事实是:如上写法中 has_value_type 始终接受两个参数,第二个是 void,编译器找到主模板后再用模式匹配找 specialization,有则用之。
    linux40
        12
    linux40  
       2017-09-04 07:51:56 +08:00 via Android
    原来很久以前我看 glibc++里有个 void_t,原来是真么回事。。。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1403 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 23:47 · PVG 07:47 · LAX 15:47 · JFK 18:47
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.