设为首页收藏本站|繁體中文

Project1

 找回密码
 注册会员
搜索
查看: 3032|回复: 9
打印 上一主题 下一主题

[原创发布] 【慎入】造轮黑科技:C++中的traits和SFINAE,了解一下?

[复制链接]

Lv3.寻梦者

梦石
0
星屑
1803
在线时间
133 小时
注册时间
2013-10-6
帖子
193
跳转到指定楼层
1
发表于 2018-3-29 22:51:02 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式

加入我们,或者,欢迎回来。

您需要 登录 才可以下载或查看,没有帐号?注册会员

x
本帖最后由 不死鸟之翼 于 2018-3-29 23:55 编辑

为了方便未登录用户阅读,同样的文章已经放到这里 https://azurefx.name/article/cpp-traits-and-sfinae/

大扎好,我系喵^3,今天为大家介绍一下【今天】的C++造轮子时与编译器玩耍用到的两个小技巧。)咳
C++是静态类型的语言,所有对象的类型要在编译期决定。这给造轮子带来了一些麻烦,比如,在编写泛型方法的时候有时需要判断目标类型是否支持对应的操作
但是,C++这语言有些问题:缺少Metaclass,语言自省能力不足,再加上编译期必须推导出所有变量的类型,导致这种“判断”不好实现。(C++的所有高级抽象原则都是在尽量不造成太大开销的情况下实现的,这也是为什么C++这么快的原因之一)
不过由于C++的模板推导能力很强大,还是可以使用一些很hack的方式实现这类要求的。
标准库提供了大量的helper设施来判断某个类型的“特性”,比如是否可平凡析构、是否是数值类型,等等。
这里我没有用到它们,而是尝试自己推导了一组,虽然实现未必漂亮但过编译还是没问题的……233

首先我们定义两个类型,Foo和Bar。状元和喵喵喵友情出演

【EDIT:应某不愿透露姓名的我以为是兰兰的人要求,以下的状元请自行脑补成lanza,雾】


Foo有两个double类型的成员taroxd和kuerlulu,而Bar只有一个taroxd,还是int类型的。



然后写一个函数fun,并分别用Foo和Bar类型对象作为参数调用它,经过一些蜜汁操作之后,我们可以区分fun中的抽象类型Ty是不是Foo类型,以及它有没有kuerlulu这个值(通过给Foo打了一个has_kuerlulu实现的,其实可以修改一下,直接去判断它有没有这个成员)
运行输出如下





看懂了嘛?懂了可以Ctrl+W了,不懂的话不着急,冷静分析.webp
为了实现fun的效果,我们需要在编译的时候计算出Ty的具体类型、以及它有没有kuerlulu这个成员,并调用正确的重载函数。如果不这么做,对于Bar这个类型,是不存在Bar::kuerlulu的,因此fun过不了编译。
因为一切的判断都是静态的,是充分利用编译器类型推导的结果,所以思维方式要转变一下,这里的“C++”其实并不是跑起来的C++,更像是一个函数式语言…
(顺便一提,利用模板类型推导可以进行计算,并且被证明是图灵完全的,感兴趣的可以写个编译期阶乘之类的~小心不要把编译器给爆栈了哦)

首先做两个辅助类型

其实就是两个空的结构体,里面的using是在结构体内部的可见域里定义类型的别名(例如using uint = unsigned int;),并不是成员啦。
还有一个括号运算符重载,用来获得对应的布尔值true/false
注意到static_false里没有is_true,static_true里没有is_false,具体有什么用后面再说。



然后做一个is_foo类型来判断。这里利用了C++的模板特化
假设有一个模板结构类型is_foo<T>,然后我们特化了一个版本叫is_foo<Foo>,那么编译器在模板实例化时会选择“更完美匹配”的版本,这里的效果就是其他类型会匹配到第一个,Foo类型会匹配到第二个。
自然,如果用int实例化,is_foo<int>里面的value就定义为static_false类型了,所以在编译期【is_foo<int>::value】这个类型就是static_false
大概就是这种思路,我们用类型定义来代替运行期的“变量”的概念

现在已经搞定了fun前半部分的is_foo操作了。由于is_foo<int>::value是一个类型,is_foo<int>::value()就是默认构造了一个is_foo<int>::value类型的对象的意思,is_foo<int>::value()()就是调用了这个对象的operator(),返回一个bool值。根据这个进行if选择就好了。
我们在写泛型轮子的时候,就可以利用这类手段去针对特定类型执行对应操作。


那教练…有没有更给力的?


我们永远不会知道我们的用户会塞进来什么样的类型呀,指明类型的名字显然不是一个好主意。更好的做法是判断一个类型是否具有某种“特性”,这就是traits,可以理解为特性萃取
事实上标准库已经这么做了。比如算法库会根据你传进来的迭代器类型,查询iterator_traits里的信息,比如你这迭代器是否支持随机访问,迭代器解引用之后的类型是什么…然后选择效率最高的算法实现。没错,都是编译期决定的。


所以我们也来写一个吧。


foo_traits_base类型是用来保存某个信息对应的两个traits默认值的。value_type定义为Ty类型里的tag_value_type的别名,而has_kuerlulu定义为static_false类型的别名。
普通版的foo_traits<Ty>直接继承了base,而Foo这个类型可以定义自己的foo_traits<Foo>特化(通常这个定义和你的Foo类是一块提供的,我是为了更清楚的叙述逻辑才放到这里的)
还是和is_foo类似的思路,foo_traits<Bar>::has_kuerlulu就是static_false类型的别名。只有Foo这个类型的has_kuerlulu是static_true的别名。这就相当于你提供一个类型Ty,然后foo_traits这个类型就可以把Ty的一些“特性”提取出来。
而value_type我们希望它表示Ty里面的成员的类型(Foo是double而Bar是int),它是从这两个结构定义里的tag_value_type获得的。可以翻上去看看

接下来就是上面这堆东西的真正用法了。

这两个版本的get_kuerlulu_if是在fun里面调用的函数重载候选。我们需要根据给fun的Ty类型里有没有kuerlulu来决定调用哪个版本。所谓的SFINAE就是“替换失败不是错误”,意思是编译器用实际类型X在匹配重载的模板候选P时,如果将模板参数Ty替换为X时会导致病式(ill-formed),那么不会产生编译错误,而是将候选P从重载候选中移除。我们就利用这个规则,让编译器在没有kuerlulu时拒绝候选2,从而接受候选1,什么都不做;当Ty拥有kuerlulu时,拒绝候选1而接受候选2。

利用SFINAE有很多种形式,比如在函数参数里加一个类型,让它在特定条件下为病式,但是这样就多了一个参数,看起来很不好(编译器会不会把它优化掉另说…)
所以这里我们选择从返回值做文章。这两个版本的返回值都是void。
Sb在实例化的时候我们放上了foo_traits<Ty>::has_kuerlulu。再强调一遍,它是个类型别名,可能是static_false<>或者static_true<> (我把它们写成了模板,默认参数其实有一个Ty=void)
为了帮助理解,我们分类讨论。
假设Sb的类型是static_false<void>:
那么根据定义,对第一个版本,static_false<void>::is_false就是void的别名,函数返回值就被替换为void,完美。
对第二个版本,由于static_false<void>中不存在is_true这个定义,因此为病式,这个版本被拒绝。
假设Sb的类型是static_true<void>,类似地,第一个版本被拒绝,第二个版本匹配上了。


到此为止,就有了fun中的那套神奇操作。




我的看法:
有效吗?有效。
为什么写法这么丑,就像做数学证明题?因为这套操作并不是语言有意设计的,纯属这个语言的类型推导过于牛逼。
有更好的写法吗?比如给C++增加更多的语言特性,编译期反射啊元类啊Concepts啊…这个就要等待语言的发展了,目前C++是一个过于复杂的语言,拥有巨量的语言特性(和坑),导致这个语言极难掌握、极难精通。←“精通C++”已经成为一个梗了,类似“PHP是最好的语言”一样的存在。作为萌新的我自己连“熟练”都不敢说呢…听说有人想21天?)雾
所以再增加新特性的话必须慎之又慎(不考虑兼容性的话甚至还要砍吧),期待那群语言律师在C++20(如果不鸽的话)能拿出更棒的设计来,让这个语言写起来更靠谱、更友好。

你的看法?
大佬们表表态吧,轻喷。本文纯属瞎编,因被本文带入奇技淫巧不归路的概不负责…

对了,忘了代码。
CPP 代码复制
  1. struct Foo {
  2.         using tag_value_type = double;
  3.         double taroxd;
  4.         double kuerlulu;
  5. };
  6. struct Bar {
  7.         using tag_value_type = int;
  8.         int taroxd;
  9. };
  10. //////////
  11. template<typename Ty = void>
  12. struct static_false {
  13.         using is_false = Ty;
  14.         constexpr bool operator()() {
  15.                 return false;
  16.         }
  17. };
  18. template<typename Ty = void>
  19. struct static_true {
  20.         using is_true = Ty;
  21.         constexpr bool operator()() {
  22.                 return true;
  23.         }
  24. };
  25. //////////
  26. template<typename>
  27. struct is_foo {
  28.         using value = static_false<>;
  29. };
  30.  
  31. template<>
  32. struct is_foo<Foo> {
  33.         using value = static_true<>;
  34. };
  35. //////////
  36. template<typename Ty>
  37. struct foo_traits_base {
  38.         using value_type = typename Ty::tag_value_type;
  39.         using has_kuerlulu = static_false<>;
  40. };
  41.  
  42. template<typename Ty>
  43. struct foo_traits:public foo_traits_base<Ty> {
  44. };
  45.  
  46. template<>
  47. struct foo_traits<Foo>:foo_traits_base<Foo> {
  48.         using has_kuerlulu = static_true<>;
  49. };
  50. //////////
  51. template<typename Sb, typename Ty>
  52. typename Sb::is_false get_kuerlulu_if(Ty& obj) {
  53. }
  54.  
  55. template<typename Sb, typename Ty>
  56. typename Sb::is_true get_kuerlulu_if(Ty& obj) {
  57.         cout << "We also have kuerlulu=" << obj.kuerlulu << "!" << endl;
  58. }
  59.  
  60. template<typename Ty>
  61. void fun(Ty obj) {
  62.         if (is_foo<Ty>::value()()) {
  63.                 cout << "Foo!" << endl;
  64.         }
  65.         else {
  66.                 cout << "Not foo!" << endl;
  67.         }
  68.         foo_traits<Ty>::value_type v = obj.taroxd;
  69.         cout << v << endl;
  70.         get_kuerlulu_if<foo_traits<Ty>::has_kuerlulu>(obj);
  71. }
  72. //////////
  73. int main() {
  74.         Foo f = { 1.1,2.2 };
  75.         Bar b = { 3 };
  76.         fun(f);
  77.         fun(b);
  78. }

评分

参与人数 1+1 收起 理由
Vortur + 1 QAQ完全看不懂...好难过

查看全部评分

←你看到一只经常潜水的萌新。

Lv4.逐梦者

梦石
0
星屑
9280
在线时间
2504 小时
注册时间
2011-5-20
帖子
15389

开拓者

2
发表于 2018-3-29 23:24:13 | 只看该作者
有一种花时间证明地球是圆的一样的感觉···

点评

1是入坑的人需要想办法确定的,2交给你们这些大触来(滑稽)  发表于 2018-3-30 10:46
差不多吧,但掌握了还是很实用的。自己造一遍轮子就会知道 1 How it works; 2 Can we make it better?  发表于 2018-3-29 23:48
[img]http://service.t.sina.com.cn/widget/qmd/5339802982/c02e16bd/7.png
回复 支持 反对

使用道具 举报

Lv5.捕梦者 (版主)

梦石
28
星屑
10170
在线时间
4673 小时
注册时间
2011-8-22
帖子
1279

开拓者

3
发表于 2018-3-30 08:51:28 | 只看该作者
嗯感觉实现还是挺直接的……大概就是很多逻辑都在编译期完成了。
之前还见过玩递归的(怕写错于是stackoverflow上找了一个:)

  1. template<class none = void>
  2. constexpr int f()
  3. {
  4.     return 0;
  5. }
  6. template<int First, int... Rest>
  7. constexpr int f()
  8. {
  9.     return First + f<Rest...>();
  10. }
  11. int main()
  12. {
  13.     f<1, 2, 3>();
  14.     return 0;
  15. }
复制代码


实际写的时候还是习惯根据需求给相应的类加上bool isXXX()……

评分

参与人数 1+1 收起 理由
不死鸟之翼 + 1

查看全部评分

回复 支持 反对

使用道具 举报

Lv3.寻梦者

梦石
0
星屑
1803
在线时间
133 小时
注册时间
2013-10-6
帖子
193
4
 楼主| 发表于 2018-4-2 23:04:58 | 只看该作者
本帖最后由 不死鸟之翼 于 2018-4-2 23:16 编辑
⑨姐姐 发表于 2018-3-30 08:51
嗯感觉实现还是挺直接的……大概就是很多逻辑都在编译期完成了。
之前还见过玩递归的(怕写错于是stackover ...


倒不太一样…这个是C++14的特性,叫Variadic templates 不过打包模板参数也确实是编译期展开的
一般我用这个做类型安全的printf 大概是这样(用Win32API写屏的)

重载了operator<<(ostream&)的类型都能写进去 比如各种矩阵类)好像暴露了什么

CPP 代码复制
  1. template<typename T>
  2. void print(const T& arg) {
  3.         wstringstream ss;
  4.         ss << arg;
  5.         HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
  6.         DWORD charsWritten;
  7.         wstring str = ss.str();
  8.         WriteConsoleW(hStdOut, str.c_str(), str.size(), &charsWritten, NULL);
  9. }
  10.  
  11. template<typename T1, typename T2, typename... Ts>
  12. void print(const T1& arg1, const T2& arg2, Ts... rest) {
  13.         wstringstream ss;
  14.         ss << arg1 << arg2;
  15.         print(ss.str(), rest...);
  16. }
  17.  
  18. int main() {
  19.         print(L"这是", 1, L"个类型安全的", L"printf\n");
  20. }







正经的TMP算斐波那契数列的话大概是这样

CPP 代码复制
  1. template<int N>
  2. struct Fibonacci {
  3.         enum { value = Fibonacci<N - 2>::value + Fibonacci<N - 1>::value };
  4. };
  5.  
  6. template<>
  7. struct Fibonacci<1> {
  8.         enum { value = 1 };
  9. };
  10.  
  11. template<>
  12. struct Fibonacci<2> {
  13.         enum { value = 1 };
  14. };
  15.  
  16. int main() {
  17.         cout << Fibonacci<5>::value << endl;
  18. }


给两个特化就行了)没有constexpr的时代就是这么简单暴力

评分

参与人数 1+1 收起 理由
⑨姐姐 + 1

查看全部评分

←你看到一只经常潜水的萌新。
回复 支持 1 反对 0

使用道具 举报

Lv1.梦旅人

梦石
0
星屑
163
在线时间
249 小时
注册时间
2014-7-18
帖子
44
5
发表于 2018-4-3 21:51:29 | 只看该作者
cpp......嗯,反正咱比较懒,坐等标准更新……
Role Play Games had saved me, but I can never save them.
回复 支持 反对

使用道具 举报

Lv4.逐梦者

梦石
0
星屑
8080
在线时间
7346 小时
注册时间
2010-7-16
帖子
4915

开拓者

6
发表于 2018-4-4 11:45:59 | 只看该作者
本帖最后由 熊的选民 于 2018-4-4 11:48 编辑

为什么又是反人类的foo啊bar啊的,搞点简单易懂的dog、cat不是更好吗?
回复 支持 反对

使用道具 举报

Lv3.寻梦者

孤独守望

梦石
0
星屑
3132
在线时间
1535 小时
注册时间
2006-10-16
帖子
4321

开拓者贵宾

7
发表于 2018-4-4 12:36:36 | 只看该作者
熊的选民 发表于 2018-4-4 11:45
为什么又是反人类的foo啊bar啊的,搞点简单易懂的dog、cat不是更好吗?

Alice、Bob、Eason 等用户向你发送了赞

点评

boob表示不服  发表于 2018-4-5 11:43
菩提本非树,明镜本非台。回头自望路漫漫。不求姻缘,但求再见。
本来无一物,何处惹尘埃。风打浪吹雨不来。荒庭遍野,扶摇难接。
不知道多久更新一次的博客
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册会员

本版积分规则

拿上你的纸笔,建造一个属于你的梦想世界,加入吧。
 注册会员
找回密码

站长信箱:[email protected]|手机版|小黑屋|无图版|Project1游戏制作

GMT+8, 2024-11-24 22:37

Powered by Discuz! X3.1

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表