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

2017-09-03 22:19:43 +08:00
 scinart

我想知道一个类里有没有定义 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;
}
2492 次点击
所在节点    C
12 条回复
yorTX9t
2017-09-03 22:22:51 +08:00
你用的是什么编译器? VC 在 two phase name look up 的实现上有 bug,你用的是这个么?还没看代码,只是猜测。
gnaggnoyil
2017-09-03 22:45:13 +08:00
has_value_type2 的偏特化错了.has_value_type<A, void>并不能选取 has_value_type<A, int>作为自己的特化实例.
scinart
2017-09-03 23:02:28 +08:00
@yorTX9t 我 clang 4.0 和 gcc 7.1

@gnaggnoyil 我想偏特化,但是编译器没给我偏特化,所以是不是编译器应该给我报个错?但是我这没错没警告,那 clang 和 gcc 背后干了啥?
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
2017-09-03 23:47:35 +08:00
@scinart 因为你 has_value_type2 的写法就弄出来了一个不偏特化也是 well-formed 的上下文啊……具体原因我 2L 已经说了,has_value_type2<A>因为是 has_value_type2<A, void>,而 void != typename A::value_type,所以是用的主模板版本进行的实例化.
scinart
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
2017-09-04 00:12:40 +08:00
@scinart 因为试图对偏特化模板进行匹配的实例不是 has_value_type2<A, int>,而是 has_value_type2<A, void>……你注意到主模板第二个参数的默认参数是什么了吗……
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
2017-09-04 00:54:55 +08:00
@scinart 啊,上边的回复 at 错了人
yangff
2017-09-04 01:22:29 +08:00
因为你用模板的姿势有点偏差…… 首先你可能误解了模板的特化…… 模板的特化实际上是模式匹配,而不是某种自动填充……
你可以试试 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
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
2017-09-04 07:51:56 +08:00
原来很久以前我看 glibc++里有个 void_t,原来是真么回事。。。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/387904

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX