【转载】自动类型推导
【本文转自】:
作者: 苏丙榅
链接: https://subingwen.cn/cpp/autotype/
来源: 爱编程的大丙
在 C++11 中增加了很多新的特性,比如可以使用 auto 自动推导变量的类型,还能够结合 decltype 来表示函数的返回值。使用新的特性可以让我们写出更加简洁,更加现代的代码。
1. auto
在 C++98 标准中 auto 和 static 是对应的,表示变量是自动存储的,但是非 static 的局部变量默认都是自动存储的,因此这个关键字变得非常鸡肋,在 C++11 中他们赋予了新的含义,使用这个关键字能够像别的语言一样自动推导出变量的实际类型。
1.1 推导规则
C++11 中 auto 并不代表一种实际的数据类型,只是一个类型声明的 “占位符”,auto 并不是万能的在任意场景下都能够推导出变量的实际类型,使用 auto 声明的变量必须要进行初始化,以让编译器推导出它的实际类型,在编译时将 auto 占位符替换为真正的类型
。使用语法如下:
auto 变量名 = 变量值;
根据上述语法,来列举一些简单的例子:
auto x = 3.14; // x 是浮点型
auto y = 520; // y 是整形 int
auto z = 'a'; // z 是字符型 char
auto nb; // error,变量必须要初始化
auto double nbl; // 语法错误, 不能修改数据类型
1.2 auto 的限制
auto 关键字并不是万能的,在以下这些场景中是不能完成类型推导的:
限制1:不能作为函数参数使用。因为只有在函数调用的时候才会给函数参数传递实参,auto 要求必须要给修饰的变量赋值,因此二者矛盾。
int func(auto a, auto b) // ERROR: a parameter cannot have a type that contains 'auto'
{
std::cout << "a: " << a <<", b: " << b << std::endl;
}
限制2:不能用于类的非静态成员变量的初始化
class Test
{
auto v1 = 0; // ERROR: 'v1': a non-static data member cannot have a type that contains 'auto'
static auto v2 = 0; // ERROR: 'Test::v2': a static data member with an in - class initializer must have non - volatile const integral type or be specified as 'inline'
static const auto v3 = 10; // ok
}
限制3:不能使用 auto 关键字定义数组
int func()
{
int array[] = {1,2,3,4,5}; // 定义数组
auto t1 = array; // ok, t1被推导为 int* 类型
auto t2[] = array; // error, auto无法定义数组
auto t3[] = {1,2,3,4,5}; // error, auto无法定义数组
}
限制4: 无法使用 auto 推导出模板参数
template <typename T>
struct Test{}
int func()
{
Test<double> t;
Test<auto> t1 = t; // error, 无法推导出模板类型
return 0;
}
1.3 auto 的应用
在 C++11 之前,定义了一个 stl 容器之后,遍历的时候常常会写出这样的代码:
#include <map>
int main()
{
std::map<int, std::string> person;
std::map<int, std::string>::iterator it = person.begin();
for (; it != person.end(); ++it)
{
// do something
}
return 0;
}
可以看到在定义迭代器变量 it 的时候代码是很长的,写起来就很麻烦,使用了 auto 之后,就变得清爽了不少:
#include <map>
int main()
{
std::map<int, std::string> person;
// 代码简化1
for (auto it = person.begin(); it != person.end(); ++it) {
// do something
}
// 代码简化2
for (auto it : person) {
// do something
}
return 0;
}
2. decltype
在某些情况下,不需要或者不能定义变量,但是希望得到某种类型,这时候就可以使用 C++11 提供的 decltype 关键字了,它的作用是在编译器编译的时候推导出一个表达式的类型,语法格式如下:
decltype (表达式)
decltype 是 “declare type” 的缩写,意思是 “声明类型”。decltype 的推导是在编译期完成的,它只是用于表达式类型的推导,并不会计算表达式的值。来看一组简单的例子:
int a = 10;
decltype(a) b = 99; // b -> int
decltype(a+3.14) c = 52.13; // c -> double
decltype(a+b*c) d = 520.1314; // d -> double
可以看到 decltype 推导的表达式可简单可复杂,在这一点上 auto 是做不到的,auto 只能推导已初始化的变量类型。
2.1 推导规则
场景1:表达式为普通变量或者普通表达式或者类表达式,在这种情况下,使用 decltype 推导出的类型和表达式的类型是一致的。
#include <iostream>
#include <string>
class Test
{
public:
std::string text;
static const int value = 110;
};
int main()
{
int x = 99;
const int &y = x;
decltype(x) a = x;
decltype(y) b = x;
decltype(Test::value) c = 0;
Test t;
decltype(t.text) d = "hello, world";
return 0;
}
- 变量 a 被推导为 int 类型
- 变量 b 被推导为 const int & 类型
- 变量 c 被推导为 const int 类型
- 变量 d 被推导为 std::string 类型
场景2:表达式是函数调用,使用 decltype 推导出的类型和函数返回值一致。
class Test{...};
//函数声明
int func_int(); // 返回值为 int
int& func_int_r(); // 返回值为 int&
int&& func_int_rr(); // 返回值为 int&&
const int func_cint(); // 返回值为 const int
const int& func_cint_r(); // 返回值为 const int&
const int&& func_cint_rr(); // 返回值为 const int&&
const Test func_ctest(); // 返回值为 const Test
//decltype类型推导
int n = 100;
decltype(func_int()) a = 0;
decltype(func_int_r()) b = n;
decltype(func_int_rr()) c = 0;
decltype(func_cint()) d = 0;
decltype(func_cint_r()) e = n;
decltype(func_cint_rr()) f = 0;
decltype(func_ctest()) g = Test();
- 变量 a 被推导为 int 类型
- 变量 b 被推导为 int& 类型
- 变量 c 被推导为 int&& 类型
- 变量 d 被推导为 int 类型
- 变量 e 被推导为 const int & 类型
- 变量 f 被推导为 const int && 类型
- 变量 g 被推导为 const Test 类型
函数 func_cint () 返回的是一个纯右值(在表达式执行结束后不再存在的数据,也就是临时性的数据),对于纯右值而言,只有类类型可以携带const、volatile限定符,除此之外需要忽略掉这两个限定符,因此推导出的变量 d 的类型为 int 而不是 const int。
场景3:表达式是一个左值,或者 被括号 () 包围,使用 decltype 推导出的是表达式类型的引用(如果有 const、volatile 限定符不能忽略)。
#include <iostream>
#include <vector>
class Test
{
public:
int num;
};
int main() {
const Test obj;
//带有括号的表达式
decltype(obj.num) a = 0;
decltype((obj.num)) b = a;
//加法表达式
int n = 0, m = 0;
decltype(n + m) c = 0;
decltype(n = n + m) d = n;
return 0;
}
- obj.num 为类的成员访问表达式,符合场景 1,因此 a 的类型为 int
- obj.num 带有括号,符合场景 3,因此 b 的类型为 const int&
- n+m 得到一个右值,符合场景 1,因此 c 的类型为 int
- n=n+m 得到一个左值 n,符合场景 3,因此 d 的类型为 int&
2.2 decltype 的应用
关于 decltype 的应用多出现在泛型编程中。比如我们编写一个类模板,在里边添加遍历容器的函数,操作如下:
#include <list>
template <class T>
class Container
{
public:
void func(T& c)
{
for (m_it = c.begin(); m_it != c.end(); ++m_it)
{
std::cout << *m_it << " ";
}
std::cout << std::endl;
}
private:
??? m_it; // 这里不能确定迭代器类型
};
int main()
{
const std::list<int> lst;
Container<const std::list<int>> obj;
obj.func(lst);
return 0;
}
在程序的 m_it 这里出了问题,关于迭代器变量一共有两种类型:只读(T::const_iterator)和读写(T::iterator),有了 decltype 就可以完美的解决这个问题了,当 T 是一个 非 const 容器得到一个 T::iterator,当 T 是一个 const 容器时就会得到一个 T::const_iterator。
#include <list>
#include <iostream>
template <class T>
class Container
{
public:
void func(T& c)
{
for (m_it = c.begin(); m_it != c.end(); ++m_it)
{
std::cout << *m_it << " ";
}
std::cout << std::endl;
}
private:
decltype(T().begin()) m_it; // 使用 decltype 成功推导 T 的类型
};
int main()
{
const std::list<int> lst{ 1,2,3,4,5,6,7,8,9 };
Container<const std::list<int>> obj;
obj.func(lst);
return 0;
}
3. 返回类型后置
在泛型编程中,可能需要通过参数的运算来得到返回值的类型,比如下面这个场景:
#include <iostream>
// R->返回值类型, T->参数1类型, U->参数2类型
template <typename R, typename T, typename U>
R add(T t, U u)
{
return t + u;
}
int main()
{
int x = 520;
double y = 13.14;
// auto z = add<decltype(x + y), int, double>(x, y);
auto z = add<decltype(x + y)>(x, y); // 简化之后的写法
std::cout << "z: " << z << std::endl;
return 0;
}
关于返回值,从上面的代码可以推断出和表达式 t+u 的结果类型是一样的,因此可以通过通过 decltype 进行推导,关于模板函数的参数 t 和 u 可以通过实参自动推导出来,因此在程序中就也可以不写。虽然通过上述方式问题被解决了,但是解决方案有点过于理想化,因为对于调用者来说,是不知道函数内部执行了什么样的处理动作的。
因此如果要想解决这个问题就得直接在 add 函数身上做文章,先来看第一种写法:
template <typename T, typename U> //ERROR: error C2065: 't': undeclared identifier
decltype(t+u) add(T t, U u)
{
return t + u;
}
当我们在编译器中将这几行代码改出来后就直接报错了,因此 decltype 中的 t 和 u 都是函数参数,直接这样写相当于变量还没有定义就直接用上了。
在C++11中增加了返回类型后置语法,说明白一点就是将 decltype 和 auto 结合起来完成返回类型的推导。其语法格式如下:
// 符号 -> 后边跟随的是函数返回值的类型
auto func(参数1, 参数2, ...) -> decltype(参数表达式)
通过对上述返回类型后置语法代码的分析,得到结论:auto 会追踪 decltype() 推导出的类型,因此上边的 add() 函数可以做如下的修改:
#include <iostream>
template <typename T, typename U>
// 返回类型后置语法
auto add(T t, U u) -> decltype(t+u) //OK
{
return t + u;
}
int main()
{
int x = 520;
double y = 13.14;
// auto z = add<int, double>(x, y);
auto z = add(x, y); // 简化之后的写法
std::cout << "z: " << z << std::endl;
return 0;
}