类型基础
在回顾模板之前,需要明确一个概念:模板编程是针对类型的计算。。这和我们平时的代码不同,我们平时写的程序都是针对数据的。
在模板元编程中,typename
用于定义类型;using
用于给模板类型赋值,注意这里的赋值和变量的赋值意义不同。参考下面的代码:
#include <iostream>
using LL = long long;
struct Foo {
using T1 = int;
using T2 = float;
};
int main() {
LL n = 1e10;
std::cout << n << std::endl;
Foo::T1 a = 10;
Foo::T2 b = 100.0;
std::cout << a << ", " << b << std::endl;
std::cout << sizeof(Foo) << std::endl; // 类型不占用结构体的size
return 0;
}
/*
10000000000
10, 100
1
*/
模板一般和 struct
共同使用,类型不占用结构体的size。类型的可见性和成员变量的可见性一致,比如:
#include <iostream>
class Foo {
using T = int;
public:
static void foo() {
T a = 10; // 这里可用,非 static 同样可用 T
std::cout << a << std::endl;
}
};
int main() {
// Foo::T a = 10; // private 函数不可用
Foo::foo();
return 0;
}
根绝这个例子,我们可以把类型认为是不占用空间的成员。因此,如果是继承关系,那么子类会继承父类的类型,可见性和成员变量的一致。代码实例:
#include <iostream>
struct Base {
using T1 = int;
};
struct Derived: Base {
using T2 = float;
};
int main() {
Derived::T1 a = 10;
Derived::T2 b = 1.1;
std::cout << a << ", " << b << std::endl;
return 0;
}
之后,我们再看typename
关键字,用于定义类型:
#include <iostream>
template<typename T>
struct Foo {
T a;
};
int main() {
Foo<int> foo{};
foo.a = 10;
std::cout << foo.a << std::endl;
return 0;
}
对于这种情况,class
也可以胜任。但是typename
有个更特殊的功能,用于声明类型:
#include <iostream>
template<typename T>
struct Foo {
void foo() {
typename T::t a = 10; // T::t显式说明这是类型(1)
std::cout << a << std::endl;
}
};
struct Type {
using t = int;
};
int main() {
Foo<Type> f{};
f.foo();
Foo<int> f1{}; // 这里声明不会报错(2)
// f1.foo(); // 这里会报错(3)
return 0;
}
上面的代码(1)中,typename
显式说明 T::t
是类型。因为,T::t 也可以作为成员函数或者成员变量:
#include <iostream>
template<typename T>
struct Foo {
void foo() {
T::t(); // 这里作为成员变量了
}
};
struct Type {
static void t() {
std::cout << "type test" << std::endl;
}
};
int main() {
Foo<Type> foo{};
foo.foo();
return 0;
}
SFINAE
Substitution Faile Is Not An Error
C++在执行模板匹配的时候,如果有多个候选项,那么当一个匹配不上的时候,不会报错,而是会继续匹配其他的,直到有最佳的匹配项,或者都匹配不上报错为止。
当然,也有优先匹配的原则,这个就是模板偏化和特化,这里不再特殊介绍。
直接给出一个 SFINAE 的例子:
#include <iostream>
#include <type_traits>
template<typename T>
class Future {};
template<typename T>
struct IsFuture: std::false_type {};
template<typename T>
struct IsFuture<Future<T>>: std::true_type {};
int main() {
// 这里匹配到 Future<T>,所以类型是 True
std::cout << IsFuture<Future<int>>::value << std::endl;
// 匹配不到Future,会去默认的
std::cout << IsFuture<int>::value << std::endl;
return 0;
}
std::enable_if
SFNIAE 最经典的例子,就是std::enable_if
了,具体用法看手册,给出一个简单的例子:
#include <iostream>
#include <type_traits>
template<typename T>
std::enable_if_t<
std::is_same_v<T, int>,
double> foo(T n) {
return n * 2.0;
}
template<typename T>
std::enable_if_t<
std::is_same_v<T, double>,
int> foo(T n) {
return int(n) % 2;
}
int main() {
std::cout << foo(10) << std::endl; // --> 20
std::cout << foo(1.5) << std::endl; // --> 1
// std::cout << foo("aaa") << std::endl; // (1)
return 0;
}
(1)没有对应的定义,无法生成定义。
上面是个简单的例子,当然了,这个通过函数重载也可以做。我们给出一个无法通过函数重载做的例子:
#include <iostream>
#include <type_traits>
template<typename F, typename ...Args>
std::enable_if_t<
std::is_same_v<std::invoke_result_t<F, Args...>, int>,
double> foo(F &&f, Args &&... args) {
int n = std::forward<F>(f)(std::forward<Args>(args)...);
return n * 2.0;
}
template<typename F, typename ...Args>
std::enable_if_t<
std::is_same_v<std::invoke_result_t<F, Args...>, double>,
int> foo(F &&f, Args &&... args) {
double n = std::forward<F>(f)(std::forward<Args>(args)...);
return int(n) % 2;
}
int f(int, double) {
return 10;
}
double f1(double, int) {
return 3.0;
}
void f2() {}
int main() {
int a = 10;
std::cout << foo(f, a, 1.0) << std::endl;
std::cout << foo(f1, 10.0, a) << std::endl;
// std::cout << foo(f2) << std::endl; // 错误,没有这种类型重载
return 0;
}
意义是,foo
函数的参数是一个函数f
和f
对应的值,之后根据f
的返回值来特化出不同的版本。这种例子,是函数重载根本无法实现的。
参考文档
http://kaiyuan.me/2018/05/08/sfinae/
https://marvinsblog.net/post/2019-09-11-cpp-sfinae-intro/
https://zhuanlan.zhihu.com/p/21314708