问题
假设我们有三个结构体:
struct A { int a; };
struct B { float a; };
struct C { int b; };
如何能够拥有一个函数f
,对A
类型输出int a
成员的值,对B
类型输出float a
,对C
类型输出int b
呢?
主程序
下面的主程序代码用于测试我们的需求:
A a{1};
B b{2.2f};
C c{3};
f(a);
f(b);
f(c);
预期输出:
1
2.2
3
第一想法:函数重载
看上去多简单呀,不用多想,直接敲代码!
// f for `int a`
void f(A x) {
printf("%d\n", x.a);
}
// f for `float a`
void f(B x) {
printf("%f\n", x.a);
}
// f for `int b`
void f(C x) {
printf("%d\n", x.b);
}
函数重载简单直接,不过f
函数的使用范围被限制到了A
/B
/C
这三个结构体上,并没有做到像鸭子类型那样处理问题。
换句话说,如果我们想对所有具有int a
的结构体应用第一个f
函数,而不是只针对A
,函数重载就难以做到了。
问题升级
我们现在要求f
函数,对包含int a
成员的结构体输出int a
的值,对包含float a
成员的结构体输出float a
的值,对包含int b
成员的结构体输出int b
的值。
比如我们现在又多了一个类:
struct D { float a; float b; };
甚至用户可以自定义任何类似的结构体,只要满足问题的成员要求,均应能被f
所处理。
主程序
我们在主程序中增加了对D
的处理:
A a{1};
B b{2.2f};
C c{3};
D d{4.4f, 4.4f}; // <<<<<
f(a);
f(b);
f(c);
f(d); // <<<<<
预期输出:
1
2.2
3
4.4
第一想法:函数模板
都知道模板大法好,那我们就先随心所欲写几个函数模板吧:
// f for `int a`
template <class T>
void f(T x) { printf("%d\n", x.a); }
// f for `float a`
template <class T>
void f(T x) { printf("%f\n", x.a); }
// f for `int b`
template <class T>
void f(T x) { printf("%d\n", x.b); }
嗯,显然是想简单了,编译之后会发现
redefinition of 'template<class T> void f(T)'
这三个函数签名完全一样,编译器怎么知道调用谁呢,肯定是不可以的。
深度思考:SFINAE
我们需要根据成员的存在性,甚至成员的类型,来分配不同的函数以调用。
如果仅仅使用简单的函数的重载,我们只能一个个地枚举所有相关类型来定义函数。
然而当类型并不完全由我们掌握时,或者说用户自定义的类型也可以被应用时,就无法使用重载来做到了。
函数模板肯定是必须要用到的,但用起来也不能想当然,约束条件需要加上。
众所周知,函数模板具有SFINAE特性,可以根据不同情况来启用不同的函数。
SFINAE即“替换失败不是错误” (Substitution Failure Is Not An Error),当模板类型推导/替换失败时,不会出现编译错误,而是忽略/丢弃掉这个函数模板。
// f for `int a`
template <class T>
std::enable_if_t<is_A<T>::value> f(T x) {
printf("%d\n", x.a);
}
// f for `float a`
template <class T>
std::enable_if_t<is_B<T>::value> f(T x) {
printf("%f\n", x.a);
}
// f for `int b`
template <class T>
std::enable_if_t<is_C<T>::value> f(T x) {
printf("%d\n", x.b);
}
这里我们又用到了std::enable_if_t<bool, T = void>
(参见enable_if)。
当其中的第一个bool
类型模板参为true
时,其类型就是T
;否则,其类型不存在(替换失败),所在的模板函数被忽略/丢弃。
这样就很清楚了,只有满足is_A
的类型,才能成功匹配到第一个f
函数;其他几个看起来重复的重载函数,实际上都会被丢弃,因为他们的返回值类型不能被成功推断出来。
所以,问题转化为,如何编写is_A
等类型分类辅助类呢?
这时候,只能施展Type Traits黑魔法了!
类型分类辅助类
我们来尝试定义一下:
template <class T>
struct is_A : public std::is_same<decltype(T::a), int> {};
template <class T>
struct is_B : public std::is_same<decltype(T::a), float> {};
template <class T>
struct is_C : public std::is_same<decltype(T::b), int> {};
std::is_same<A, B>
(参见is_same)基于std::bool_constant<true | false>
,也就是std::integral_constant<bool, true | false>
(参见integral_constant),其拥有一个成员constexpr bool value
。
仅当A与B类型一致时,其为std::true_type
,即其value == true
。
然而,编译失败!
<source>: In instantiation of 'struct is_C<A>':
<source>:41:34: required by substitution of 'template<class T> std::enable_if_t<is_C<T>::value> f(T) [with T = A]'
<source>:51:6: required from here
<source>:25:44: error: 'b' is not a member of 'A'
25 | struct is_C : public std::is_same<decltype(T::b), int> {};
| ^
啊!这里并非函数模板的部分,并不存在SFINAE特性,当我们调用is_C<A>
时,因为A
不存在b
成员,所以编译直接失败。
这可怎么办?
挪到函数模板中去
既然函数模板有SFINAE特性,那我们不妨把这个decltype
挪一挪:
template <class T, class Base = std::is_same<decltype(T::a), int>>
struct is_A : public Base {};
template <class T, class Base = std::is_same<decltype(T::a), float>>
struct is_B : public Base {};
template <class T, class Base = std::is_same<decltype(T::b), int>>
struct is_C : public Base {};
运行成功!输出的结果也是完全正确的!
哈哈,看来也不过如此,没什么难度嘛!
别高兴得太早
欸,别着急,现在就泼一盆冷水!
既然is_A
这些都是类型分类辅助类(一种Type Traits
),我能不能直接用呢?
换句话说,我不想只用他们来启用不同的函数,而是直接用来判断类型是否满足我们的要求。
比如,在主程序里面加上下面这句代码:
printf("%d\n", is_A<C>::value);
什么?!编译又失败!
<source>: In function 'int main()':
<source>:18:55: error: 'a' is not a member of 'C'
18 | template <class T, class Base = std::is_same<decltype(T::a), int>>
| ^
<source>:51:24: error: template argument 2 is invalid
51 | printf("%d\n", is_A<C>::value);
| ^
这是因为在enable_if
里面使用is_A
时,如果成员不存在,会导致替换失败,触发SFINAE,丢弃相应的函数。
然而这里我们直接使用is_A
辅助类,输出其value
的值。如果类型推导失败,is_A
确实就没有定义了。
模板偏特化
那我们就要想办法为额外的情况定义辅助类等价于std::false_type
。
此时问题分为两部分,首先判断成员是否存在,然后借助is_same
判断成员类型。如果前者首先得到了判断,后者其实并不需要SFINAE。
那么首先,我们定义辅助模板类is_A
,在默认的情况下等价于std::false_type
:
template <class T, class = void>
struct is_A : public std::false_type {};
然后利用模板偏特化,对存在成员a
的类型T
特殊处理:
template <class T>
struct is_A<T, std::void_t<decltype(T::a)>> : public std::is_same<decltype(T::a), int> {};
可以看到,模板偏特化保证了这里的is_A
只针对存在成员T::a
,而后面的继承则声明该类等价于std::is_same<decltype(T::a), int>
。
由于偏特化的保证,后面的等价关系就不必担心T::a
不存在了。
is_B
和is_C
同理:
template <class T, class = void>
struct is_B : public std::false_type {};
template <class T>
struct is_B<T, std::void_t<decltype(T::a)>> : public std::is_same<decltype(T::a), float> {};
template <class T, class = void>
struct is_C : public std::false_type {};
template <class T>
struct is_C<T, std::void_t<decltype(T::b)>> : public std::is_same<decltype(T::b), int> {};
补充
如果需要同时判断多个成员是否存在,甚至结合其他的谓词进行复杂的逻辑判断,那么可以利用std::conjunction
、std::disjunction
和std::negation
组合我们的判断。
例程
在下面的例程中,is_A
判断结构体是否同时含有成员int a
和int b
,is_B
判断结构体是否同时含有成员int a
和int c
。
#include <cstdio>
#include <type_traits>
struct A {
int a;
int b;
};
struct B {
int a;
int c;
};
template <class T, class = void, class = void>
struct is_A : public std::false_type {};
template <class T>
struct is_A<T, std::void_t<decltype(T::a)>, std::void_t<decltype(T::b)>>
: public std::conjunction<std::is_same<decltype(T::a), int>,
std::is_same<decltype(T::b), int>> {};
template <class T, class = void, class = void>
struct is_B : public std::false_type {};
template <class T>
struct is_B<T, std::void_t<decltype(T::a)>, std::void_t<decltype(T::c)>>
: public std::conjunction<std::is_same<decltype(T::a), int>,
std::is_same<decltype(T::c), int>> {};
// f for `int a` & `int b`
template <class T>
std::enable_if_t<is_A<T>::value> f(T x) {
printf("%d %d\n", x.a, x.b);
}
// f for `int a` & `int c`
template <class T>
std::enable_if_t<is_B<T>::value> f(T x) {
printf("%d %d\n", x.a, x.c);
}
int main() {
A a{1, 1};
B b{2, 2};
f(a);
f(b);
}
输出:
1 1
2 2
再补充
利用C++20的新特性Concept会更简单哦~
例程
#include <concepts>
#include <cstdio>
struct A {
int a;
int b;
};
struct B {
int a;
int c;
};
template <class T>
concept TA = requires(T x) {
{ x.a } -> std::same_as<int&>;
{ x.b } -> std::same_as<int&>;
};
template <class T>
concept TB = requires(T x) {
{ x.a } -> std::same_as<int&>;
{ x.c } -> std::same_as<int&>;
};
// f for `int a` & `int b`
template <TA T>
void f(T x) {
printf("%d %d\n", x.a, x.b);
}
// f for `int a` & `int c`
template <TB T>
void f(T x) {
printf("%d %d\n", x.a, x.c);
}
int main() {
A a{1, 1};
B b{2, 2};
printf("%d %d\n", TA<A>, TA<B>);
f(a);
f(b);
}
输出:
1 0
1 1
2 2
可以看到主函数中间的那个printf
,TA<A>
就是一个bool
值,所以concept本身就可以用于类型判断啦!