C++ Notes : Effective Modern C++ 笔记 —— 类型推断:条目 1,2,3,4
0. 准备
为了可以实时的知道推导结果是否正确,首先来定义一个显示推导结果的帮助函数。由于C++本身typeid(expr).name()
的方法会去除类型的引用并且其结果可读性在gcc
下不太直观。尽管可以自定义模板来解决这个问题,为了方便还是采取条目4中直接使用 boost
库中提供的boost::typeindex::typeid_with_cvr
(保留const
,volatile
和引用信息)的方法来达到此目的。
// #include <boost/type_index.hpp> // required
#define ECHO_TYPE(T, U) \
{ \
using boost::typeindex::type_id_with_cvr; \
std::cout.width(10), std::cout.setf(std::ios::left); \
std::cout << #U << " = " \
<< type_id_with_cvr<T>().pretty_name() \
<< std::endl; \
} \
#define ECHO_T_TYPE(T) ECHO_TYPE(T, T)
#define ECHO_PARAM_TYPE(param) ECHO_TYPE(decltype(param), param)
#define ECHO() \
{ \
std::cout << std::string(30, '-') << std::endl; \
ECHO_T_TYPE(T); \
ECHO_PARAM_TYPE(param); \
} \
1. 模板推导
假定模板与调用函数定义如下:
template<typename T>
void f(paramType param);
f(expr);
T
和paramType
就是由编译器在编译期根据expr
推导得到。T
的推导结果和paramType
的声明类型有关。对于模板推导,有三种情况。
1. paramType
声明为引用或指针(非通用引用T&&
)
- 如果
expr
是引用,忽略引用部分 - 然后匹配
expr
的类型和paramType
对应的类型
// 这种情形的模板申明如下
template<typename T>
void f1(T ¶m) {
ECHO();
}
// 调用函数
int v = 10; // int
int &rv = v; // int&
const int cv = v; // const int
const int &crv = v; // const int&
f1(v); // T is int, paramType is int&
f1(rv); // T is int, paramType is int&
f1(cv); // T is const int, paramType is const int&
f1(crv); // T is const int, paramType is const int&
// 实际运行结果
/*
------------------------------
T = int
param = int&
------------------------------
T = int
param = int&
------------------------------
T = int const
param = int const&
------------------------------
T = int const
param = int const&
*/
注意,T
的类型推导受到paramType
的声明影响。如果这里paramType
声明为const T&
,如果expr
是const int&
类型,首先去引用剩下const int
,但是由于这里paramType
声明为const
,所以T
不再需要const
。T
的推导结果为int
,paramType
为const int&
。另外,这种情况下,paramType
声明为左值引用,如果没有声明为const
,是不可以推导类似于f(2)
这种情况的。因为字面值常量2
是纯右值,右值是不可以被非常量左值引用所引用的,除非是常量左值引用。即paramType
声明为const T&
,这时候f(2)
推导出T
为int
,paramType
为const int&
2. paramType
声明为通用引用(T&&
)
- 如果
expr
是左值,T
和paramType
都被推断为左值引用T&
。这是很特殊的一点!一方面,只有这种情况T
才会被推断为引用类型;另一方面,尽管paramType
被声明为右值引用类型T&&
,其推导结果任为左值引用T&
。 - 如果
expr
是右值,规则和 Case 1. 相同
// 模声明
template<typename T>
void f2(T &¶m) {
ECHO();
}
// 调用函数
f2(v); // T and paramType are both int&
f2(rv); // T and paramType are both int&
f2(cv); // T and paramType are both const int&
f2(crv); // T and paramType are both const int&
f2(std::move(v)); // T is int, paramType is int&&
f2(std::move(rv)); // T is int, paramType is int&&
f2(std::move(cv)); // T is const int, paramType is int&&
f2(std::move(crv)); // T is const int, paramType is int&&
// 运行结果
/*
------------------------------
T = int&
param = int&
------------------------------
T = int&
param = int&
------------------------------
T = int const&
param = int const&
------------------------------
T = int const&
param = int const&
------------------------------
T = int
param = int&&
------------------------------
T = int
param = int&&
------------------------------
T = int const
param = int const&&
------------------------------
T = int const
param = int const&&
*/
3. paramType
既不是引用也不是指针(pass-by-value)
- 和前面一样,如果expr是引用,则忽略引用部分
- 忽略引用部分后,如果
expr
是const
或者volatile
,const
和volatile
也忽略。
即引用,const
,volatile
通通忽略。这是合理的,因为值传递是一份拷贝,及时实参是不可修改的,函数临时拷贝的一份不应当受此限制。
注意,这里说的值传递,如果实参是指针类型,则值传递意味着传递指针地址本身。此时修饰指针指向地址所表示内容的const
不可忽略,修饰指针指向的const
可以忽略。即底层const
不可忽略。
// 模板定义
template<typename T>
void f3(T param) {
ECHO();
}
// 函数调用
f3(cv); // both T and paramType are int
f3(crv); // both T and paramType are int
const char *str1{"clion"}; // str1 is const char*
char *const str2{"clion"}; // str2 is char* const
f3(str1); // both T and paramType is const char*
f3(str2); // both T and paramType is char*
// 输出结果
/*
------------------------------
T = int
param = int
------------------------------
T = int
param = int
------------------------------
T = char const*
param = char const*
------------------------------
T = char*
param = char*
*/
数组实参
数组通常和指针联系在一起,所以数组形参通常可以声明为指针。比如,void func(int *arr);
或者void func(int arr[]);
。这两种形参,传递数组实参时数组都会退化为指针,所以其推导规则和指针是一样的。
但是,当数组形参声明为引用的时候,数组实参就不会退化为指针(通用引用也一样),这时候的推导规则就应当使用数组本身类型来推导。
int a[] = {1, 2, 3}; // int[3]
f1(a); // T is int[3], paramType is int(&)[3]
f2(a); // both T is int(&)[3]
f3(a); // both T and paramType are int*
这一特性还可以用来编译期获得数组维度
template<typename T, std::size_t N>
constexpr std::size_t getArrayLength(const T (&)[N]) {
return N;
}
auto len = getArrayLength(a);
// 编译期获得数组长度还有另外两种方法
auto len = sizeof(a) / sizeof(a[0]); // 使用sizeof
auto len = std::extent<decltype(a)>::value; // 使用std::extent
函数实参
函数实参和数组一样,也会退化为指针,推导规则和数组一样。
void funcToDeduce(int); // type: void(int)
f1(funcToDeduce); // T is void(int), paramType is void(&)(int)
f2(funcToDeduce); // both T and paramType is void(&)(int)
f3(funcToDeduce); // both T and paramType is void(*)(int)
2. auto
推导
auto
推导和模板推导的规则基本一样。auto
就相当于模板推导中的T
。auto
的推导完全可以套用模板推导规则,但是这里有一个特例。即从初始化列表推导时需要特殊注意,模板是不能从初始化列表推导的。
auto x1 = 1; // int
auto x2(1); // int
auto x3 = {1}; // std::initializer_list<int>
auto x4{1}; // int since C++17, std::initializer_list<int> before C++17
auto x5 = {1, 2}; // obviously std::initializer_list<int>
// auto x6{1, 2}; // FORBIDDEN
ECHO_PARAM_TYPE(x1);
ECHO_PARAM_TYPE(x2);
ECHO_PARAM_TYPE(x3);
ECHO_PARAM_TYPE(x4);
ECHO_PARAM_TYPE(x5);
// 输出
/*
x1 = int
x2 = int
x3 = std::initializer_list<int>
x4 = int
x5 = std::initializer_list<int>
*/
需要注意的是x4
,2014年C++标准委员会接受了提案N3932,修改了auto
对初始化列表的推导规则,所以C++17起或者在某些编译器上这里推导为int
。
要传初始化表实参给模板时,模板paramType
需要声明为初始化表,即
template <typename T>
void f4(std::initializer_list<T> &¶m) {
ECHO();
}
f4({1}); // T is int, paramType is std::initializer_list<int>&&
// 输出
/*
------------------------------
T = int
param = std::initializer_list<int>&&
*/
另外,从C++14起 auto
可以推导函数返回类型,并且可以修饰lambda表达式的形参。但是作为这些用法时,auto
的推导规则是按照模板推导规则来的。这个时候是无法推导列表初始化的
// auto generateInitList() {
// return {1}; // Compile Error, decltype(auto) 也不行
// }
// std::vector<int> vec;
// auto resetVector = [&] (const auto& v) {
// vec = v;
// };
// resetVector({1}); // 编译错误
3. decltype
推导
decltype
跟auto
和模板推断不同,它将返回精确的类型(auto
和模板在某些情况会去const
和引用),ECHO_PARAM_TYPE()
宏就是使用decltype
来获得精确类型的。
有时候模板函数的返回值需要根据参数推导,在C++11中,可以使用decltype
尾置返回类型(C++11不支持直接通过auto
推导函数返回类型)
template <typename Container, typename Index>
auto getIndexAt1(Container& c, Index i) -> decltype(c[i]) {
return c[i];
}
假设传入container为 std::vector<int>
,那么返回类型被推导为int&
。
C++14开始允许直接使用auto
从函数return
语句推导返回类型。即无需尾置返回类型了。这时就容易出错了。
template <typename Container, typename Index>
auto getIndexAt2(Container& c, Index i) {
return c[i];
}
传入container为std::vector<int>
,推导返回类型为int
(如果此时需要从返回值修改原始值,这样就做不到了)。这是因为这里的auto
推导是根据模板推导规则来的,它会丢弃引用和const
,这里需要使用decltype(auto)
template <typename Container, typename Index>
decltype(auto) getIndexAt3(Container& c, Index i) {
return c[i];
}
传入container为std::vector<int>
,推导返回类型为int&
。
PS: 更好的用法是使用完美转发,完美转发在后面的内容中提到,就此带过。
// perferct forward
template<typename Container, typename Index>
decltype(auto) getIndexAt4(Container &&c, Index i) {
return std::forward<Container>(c)[i];
}
- 若
e
是没有使用小括号()
括起来的表达式,则推断结果就是其申明的类型 - 若
e
是T
类型的xvalue
,则推断结果为T&&
,右值引用 - 若
e
是T
类型的lvalue
,则推断结果为T&
,左值引用。(同时满足1
的时候,优先1
。所以通常得加括号) - 若
e
是T
类型的prvalue
,则推断结果为T
。(字面值,临时变量)
NOTE:int a = 1, b = 2;
此时decltype((a + b))
是int
而不是int&
。因为虽然加了括号,a + b
的结果是右值。