C++ 自动类型推断

时间:2022-04-19 10:01:26

C++语言提供了自动类型推断的机制,用于简化代码书写,这是一种很不错的特性,使用autodecltype都可以完成自动类型推断的工作,而且都工作在编译期,这表示在运行时不会有任何的性能损耗。

一、auto自动类型推断

auto自动类型推断的机制和函数模板的推断机制很相似,auto就类似于模板中的T

(1.1) auto变量以传值方式初始化

一句话总结:抛弃掉对象的const和引用属性,新对象就像一个全新的副本;对于指针,会抛弃其顶层const属性,保留其底层const属性。

int main(int argc, char *argv[])
{
int a = 0;
int &a_ref = a;
const int ca = 0;
const int &ca_ref = ca;
const int *const pa = &a;
int arr[] = {1, 2, 3};
const carr[] = {1, 2, 3}; // 一、传值方式。
auto b1 = a; // b1为int, auto为int
auto b2 = a_ref; // b2为int, auto为int
auto b3 = ca; // b3为int, auto为int
auto b4 = ca_ref; // b4为int, auto为int
auto b5 = pa; // b5为const int *, auto为const int *
auto b6 = arr; // b6为int*, auto为int*
auto b7 = carr; // b7为const int*, auto为const int*
auto b8 = main; // b8为int(*)(int, char**), auto为int(*)(int,char**)
return 0;
}

(1.2) auto变量的指针或者引用类型初始化

一句话总结:引用属性被抛弃,但是const属性会被保留;对于指针类型,所有的const属性都被保留。

int main(int argc, char *argv[])
{
int a = 0;
int &a_ref = a;
const int ca = 0;
const int &ca_ref = ca;
const int *const pa = &a;
int arr[] = {1, 2, 3};
const int carr[] = {1, 2, 3}; // 二、传引用或指针方式。
auto &b1 = a; // b1为int&, auto为int
auto &b2 = a_ref; // b2为int&, auto为int
auto &b3 = ca; // b3为const int&, auto为const int
auto &b4 = ca_ref; // b4为const int&, auto为const int
auto &b5 = pa; // b5为const int *const &, auto为const int *const
auto &b6 = arr; // b6为int*&, auto为int*
auto &b7 = carr; // b7为const int *&, auto为const int *
auto &b8 = main; // b8为int(&)(int, char**), auto为int(int,char**)
return 0;
}

(1.3) auto变量的万能引用初始化

见万能引用即可:C++引用详解

二、decltype自动类型推断

decltype相对于auto来讲更加温和,因为它不会丢弃任何的一个属性(不像auto一样可能丢弃常量和引用)。当我们不想用表达式的值来初始化变量,而仅希望推断表达式的数据类型时,就可以使用decltype

(2.1) decltype推断变量的类型

decltype推断的方式非常简单:原来的类型是什么,推断的结果就是什么

int main(int argc, char *argv[])
{
int a = 0;
int &ra = a;
const int &cra = a;
int *pa = &a;
const int *cpa = pa;
const int *const ccpa = pa; decltype(a) b1 = a; // b1为int
decltype(ra) b2 = a; // b2为int&
decltype(cra) b3 = a;// b3为const int&
decltype(pa) b4 = nullptr; // b4为int*
decltype(cpa) b5 = nullptr; // b5为const int *
decltype(ccpa) b6 = nullptr; // b6为const int *const
return 0;
}

(2.2) decltype推断表达式的类型

与推断变量不同的是,decltype表达式中的内容如果是左值,那么将推断为引用类型

int main(int argc, char *argv[])
{
int a = 0;
int *pa = &a; decltype(1 + 2) b1; // 推断为int。
decltype(*pa) b2 = a; // 由于*pa是左值,因此b的类型为int&。
decltype((a)) b3 = a; // 由于(a)是表达式,且是左值,因此b的类型为int&。
return 0;
}

(2.3) decltype推断函数或函数的调用结果

个人认为这是decltype最强大的地方,因为此时并不会实际地调用函数,而仅仅会推断类型(和sizeof十分相似):

#include <string>

std::string func() { return std::string(); }

int main(int argc, char *argv[])
{
decltype(func) a; // a为函数声明:std::string();
decltype(func) *b = nullptr; // b为函数指针:std::string(*)();
decltype(func) &c = func; // c为函数引用:std::string(&)();
decltype(func().c_str()) d = nullptr; // d为const char *
// 不会实际调用func()和c_str()。
return 0;
}

(2.4) decltype的特殊用法示例

(2.4.1) 利用decltype来提高变量声明的代码兼容性

利用decltype可以在模板编程中实现数据类型的动态效果:

template<typename T>
class container_copier
{
public:
typename T::iterator _begin;
typename T::iterator _end; container_copier(typename T::iterator begin, typename T::iterator end)
: _begin(begin), _end(end) {} void copy() { /* ... */ }
};

这个模板可以应付普通的容器类型,但是无法针对常量容器对象进行操作:

#include <vector>

int main(int argc, char *argv[])
{
std::vector<int> vec = {1, 2, 3, 4, 5};
container_copier<std::vector<int>> cpr1(vec.begin(), vec.end());
cpr1.copy(); // OK. const std::vector<int> cvec = {1, 2, 3, 4, 5};
container_copier<const std::vector<int>> cpr2(vec.begin(), vec.end()); // 错误 return 0;
}

错误的原因是:对于常量容器,begin函数返回的是const_iterator,而不是iterator。如果不适用自动类型推断,我们就不得不进行偏特化:

template<typename T>
class container_copier<const T>
{
public:
typename T::const_iterator _begin;
typename T::const_iterator _end; container_copier(typename T::const_iterator begin, typename T::const_iterator end)
: _begin(begin), _end(end) {} void copy() { /* ... */ }
};

只有这样才能够通过编译。这样重复地编写偏特化代码无疑是很麻烦的。但是使用了自动类型推断之后,事情就简单多了:

#include <vector>

template<typename T>
class container_copier
{
public:
decltype(T().begin()) _begin;
decltype(T().end()) _end; container_copier(decltype(T().begin()) begin,
decltype(T().end()) end)
: _begin(begin), _end(end) {} void copy() { /* ... */ }
}; int main(int argc, char *argv[])
{
std::vector<int> vec = {1, 2, 3, 4, 5};
container_copier<std::vector<int>> cpr1(vec.begin(), vec.end());
cpr1.copy(); // OK. const std::vector<int> cvec = {1, 2, 3, 4, 5};
container_copier<const std::vector<int>> cpr2(vec.begin(), vec.end()); // OK. return 0;
}

这样,通过自动类型推断,就可以由编译器来动态地决定究竟是使用iterator还是const_iterator

(2.4.2) 利用decltype来提高函数返回类型的兼容性

有时,我们希望根据函数入参的某个方法的返回类型来决定函数的返回类型,那么就可以利用自动类型推断:

template<typename T>
auto func(T&& t) -> decltype(t.foo())
{
// ...
return t.foo();
}

这样,就可以不显示地指定返回类型,而让返回类型随着foo()方法的类型而变化。

三、decltype(auto)类型推断(c++14)

decltype(auto)的语义是:要求编译器自动推断需要自动推断的类型。使用decltype(auto)的主要目的是:由于auto会抛弃掉引用或是常量属性,但是有时却需要自动类型推断来保留这些属性,因此decltype可以将auto丢弃的属性给找回来

int main(int argc, char *argv[])
{
int a = 0;
const int &a_ref = a;
auto b1 = a_ref; // b1为int
// 如果我们想保留b1怎么办呢?
// 方法一:
decltype(auto) b2 = a_ref; // 等价于:decltype(a_ref) b2 = a_ref;
// b2为const int &
// 方法二:
auto &b3 = a_ref;
return 0;
}

一个常见的坑是:当decltype推断左值表达式时,会产生引用类型;然而如果返回一个局部变量的引用将是危险的:

decltype(auto) func()
{
int i = 30;
// ...
return(i); // 错误,因为这等价于:decltype((i)), 这将返回i的引用,引发错误。
// return i; 正确。
}