Project1
标题: 【慎入】造轮黑科技:C++中的traits和SFINAE,了解一下? [打印本页]
作者: 不死鸟之翼 时间: 2018-3-29 22:51
标题: 【慎入】造轮黑科技:C++中的traits和SFINAE,了解一下?
本帖最后由 不死鸟之翼 于 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(如果不鸽的话)能拿出更棒的设计来,让这个语言写起来更靠谱、更友好。
你的看法?
大佬们表表态吧,轻喷。本文纯属瞎编,因被本文带入奇技淫巧不归路的概不负责…
对了,忘了代码。
struct Foo {
using tag_value_type = double;
double taroxd;
double kuerlulu;
};
struct Bar {
using tag_value_type = int;
int taroxd;
};
//////////
template<typename Ty = void>
struct static_false {
using is_false = Ty;
constexpr bool operator()() {
return false;
}
};
template<typename Ty = void>
struct static_true {
using is_true = Ty;
constexpr bool operator()() {
return true;
}
};
//////////
template<typename>
struct is_foo {
using value = static_false<>;
};
template<>
struct is_foo<Foo> {
using value = static_true<>;
};
//////////
template<typename Ty>
struct foo_traits_base {
using value_type = typename Ty::tag_value_type;
using has_kuerlulu = static_false<>;
};
template<typename Ty>
struct foo_traits:public foo_traits_base<Ty> {
};
template<>
struct foo_traits<Foo>:foo_traits_base<Foo> {
using has_kuerlulu = static_true<>;
};
//////////
template<typename Sb, typename Ty>
typename Sb::is_false get_kuerlulu_if(Ty& obj) {
}
template<typename Sb, typename Ty>
typename Sb::is_true get_kuerlulu_if(Ty& obj) {
cout << "We also have kuerlulu=" << obj.kuerlulu << "!" << endl;
}
template<typename Ty>
void fun(Ty obj) {
if (is_foo<Ty>::value()()) {
cout << "Foo!" << endl;
}
else {
cout << "Not foo!" << endl;
}
foo_traits<Ty>::value_type v = obj.taroxd;
cout << v << endl;
get_kuerlulu_if<foo_traits<Ty>::has_kuerlulu>(obj);
}
//////////
int main() {
Foo f = { 1.1,2.2 };
Bar b = { 3 };
fun(f);
fun(b);
}
struct Foo {
using tag_value_type = double;
double taroxd;
double kuerlulu;
};
struct Bar {
using tag_value_type = int;
int taroxd;
};
//////////
template<typename Ty = void>
struct static_false {
using is_false = Ty;
constexpr bool operator()() {
return false;
}
};
template<typename Ty = void>
struct static_true {
using is_true = Ty;
constexpr bool operator()() {
return true;
}
};
//////////
template<typename>
struct is_foo {
using value = static_false<>;
};
template<>
struct is_foo<Foo> {
using value = static_true<>;
};
//////////
template<typename Ty>
struct foo_traits_base {
using value_type = typename Ty::tag_value_type;
using has_kuerlulu = static_false<>;
};
template<typename Ty>
struct foo_traits:public foo_traits_base<Ty> {
};
template<>
struct foo_traits<Foo>:foo_traits_base<Foo> {
using has_kuerlulu = static_true<>;
};
//////////
template<typename Sb, typename Ty>
typename Sb::is_false get_kuerlulu_if(Ty& obj) {
}
template<typename Sb, typename Ty>
typename Sb::is_true get_kuerlulu_if(Ty& obj) {
cout << "We also have kuerlulu=" << obj.kuerlulu << "!" << endl;
}
template<typename Ty>
void fun(Ty obj) {
if (is_foo<Ty>::value()()) {
cout << "Foo!" << endl;
}
else {
cout << "Not foo!" << endl;
}
foo_traits<Ty>::value_type v = obj.taroxd;
cout << v << endl;
get_kuerlulu_if<foo_traits<Ty>::has_kuerlulu>(obj);
}
//////////
int main() {
Foo f = { 1.1,2.2 };
Bar b = { 3 };
fun(f);
fun(b);
}
作者: chd114 时间: 2018-3-29 23:24
有一种花时间证明地球是圆的一样的感觉···
作者: ⑨姐姐 时间: 2018-3-30 08:51
嗯感觉实现还是挺直接的……大概就是很多逻辑都在编译期完成了。
之前还见过玩递归的(怕写错于是stackoverflow上找了一个:)
- template<class none = void>
- constexpr int f()
- {
- return 0;
- }
- template<int First, int... Rest>
- constexpr int f()
- {
- return First + f<Rest...>();
- }
- int main()
- {
- f<1, 2, 3>();
- return 0;
- }
复制代码
实际写的时候还是习惯根据需求给相应的类加上bool isXXX()……
作者: 不死鸟之翼 时间: 2018-4-2 23:04
本帖最后由 不死鸟之翼 于 2018-4-2 23:16 编辑
倒不太一样…这个是C++14的特性,叫Variadic templates 不过打包模板参数也确实是编译期展开的
一般我用这个做类型安全的printf 大概是这样(用Win32API写屏的)
重载了operator<<(ostream&)的类型都能写进去 比如各种矩阵类)好像暴露了什么
template<typename T>
void print(const T& arg) {
wstringstream ss;
ss << arg;
HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
DWORD charsWritten;
wstring str = ss.str();
WriteConsoleW(hStdOut, str.c_str(), str.size(), &charsWritten, NULL);
}
template<typename T1, typename T2, typename... Ts>
void print(const T1& arg1, const T2& arg2, Ts... rest) {
wstringstream ss;
ss << arg1 << arg2;
print(ss.str(), rest...);
}
int main() {
print(L"这是", 1, L"个类型安全的", L"printf\n");
}
template<typename T>
void print(const T& arg) {
wstringstream ss;
ss << arg;
HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
DWORD charsWritten;
wstring str = ss.str();
WriteConsoleW(hStdOut, str.c_str(), str.size(), &charsWritten, NULL);
}
template<typename T1, typename T2, typename... Ts>
void print(const T1& arg1, const T2& arg2, Ts... rest) {
wstringstream ss;
ss << arg1 << arg2;
print(ss.str(), rest...);
}
int main() {
print(L"这是", 1, L"个类型安全的", L"printf\n");
}
正经的TMP算斐波那契数列的话大概是这样
template<int N>
struct Fibonacci {
enum { value = Fibonacci<N - 2>::value + Fibonacci<N - 1>::value };
};
template<>
struct Fibonacci<1> {
enum { value = 1 };
};
template<>
struct Fibonacci<2> {
enum { value = 1 };
};
int main() {
cout << Fibonacci<5>::value << endl;
}
template<int N>
struct Fibonacci {
enum { value = Fibonacci<N - 2>::value + Fibonacci<N - 1>::value };
};
template<>
struct Fibonacci<1> {
enum { value = 1 };
};
template<>
struct Fibonacci<2> {
enum { value = 1 };
};
int main() {
cout << Fibonacci<5>::value << endl;
}
给两个特化就行了)没有constexpr的时代就是这么简单暴力
作者: M.Winderic. 时间: 2018-4-3 21:51
cpp......嗯,反正咱比较懒,坐等标准更新……
作者: 熊的选民 时间: 2018-4-4 11:45
本帖最后由 熊的选民 于 2018-4-4 11:48 编辑
为什么又是反人类的foo啊bar啊的,搞点简单易懂的dog、cat不是更好吗?
作者: IamI 时间: 2018-4-4 12:36
Alice、Bob、Eason 等用户向你发送了赞
欢迎光临 Project1 (https://rpg.blue/) |
Powered by Discuz! X3.1 |