26

现代 C++:自动类型推导

 3 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzI0NjA1MTU5Ng%3D%3D&%3Bmid=2247483871&%3Bidx=1&%3Bsn=3113bbe450eced0cf7d558493d1542b3
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

自动类型推导

现代的编程语言,不管是动态语言(JavaScript、Python 等),还是静态语言(Go、Rust 等),大都支持自动类型推导(type deduction)。

自动类型推导,通俗地讲就是定义一个变量的时候不需要明确指定类型,而是让编译器根据上下文进行推导。

在 C++11 之前,模板(template)代码就支持编译器自动类型推导。C++11 很重要的一个特性就是加强了编译器自动类型推导的能力,使之不限于模板 —— 与此相关的关键字有两个 autodecltype

auto

我们来看看 auto 关键字在 C++ 中的使用。最简单的用法,定义变量的时候不指定类型,通过初始化的值让编译器自动推导。

auto a;  // 编译不通过

auto b = 0;  // b 是 int 类型
auto c = 0ull;  // c 是 unsigned long long 类型  
auto d = "Hello World";  // d 是 const char* 类型
auto e = std::string("Hello");  // e 是 std::string 类型

auto 和容器类型、迭代器一起配合使用,可以少打很多字,代码也更简洁、清晰。

  std::vector<int> v(10, 1); 
  auto itr_begin = v.begin();  // std::vector<int>::iterator
  auto itr_end = v.end();  // std::vector<int>::iterator
  auto sz = v.size();  // std::vector<int>::size_type

如果不用自动类型推导,下面 v 的类型写起来也很麻烦。如果 b 和 e 是自定义的迭代器,不一定能用 typename std::iterator_traits<Iter>::value_type 来获得类型。

template<typename Iter>
void Process(Iter b, Iter e) {
  while (b != e) {
    auto v = *b;   // 如果不用自动类型推导,如何获得 *b 的类型
    // typename std::iterator_traits<Iter>::value_type v = *b; 
    std::cout << v << std::endl;
    ++b;
  }
}

类型推导可以和 Lambda 表达式一起愉快地使用。

auto Plus = [](int a, int b) { return a + b; };

也许有人会说,Lambda 表达式可以用一个 std::function 对象来包装。

std::function<int(int, int)> PlusFunc = [](int a, int b) { return a + b; };

但是这样做有几点不好:

  1. std::function  内部会涉及动态内存分配,性能上劣于自动类型推导的实现;

  2. 让代码看起来复杂不少;

  3. 对于泛型 Lambda 表达式,std::function  也无能为力了。

// std::function<T> 的类型没法写了
auto Plus = [](auto a, auto b) { return a + b; }; 
std::cout << Plus(3, 4) << std::endl;
std::cout << Plus(3.14, 1.11) << std::endl;
std::cout << Plus(std::string("hello"), std::string("world")) << std::endl;

某些情况下,自动类型推导还可以让你避免一些“坑”。比如:

std::unordered_map<std::string, int> m;
// ...
// 你觉得有没有问题?
for (const std::pair<std::string, int>& pa : m) {
    // ... 
}

看得出上面这段代码有什么问题吗?

上面的代码会导致复制整个 unordered_map。因为 std::unordered_map<Key, T>::value_type 的类型是 std::pair<const Key, T>。正确的写法应该是:

for (const std::pair<const std::string, Foo>& pa : m) {
    // ...
}

用自动类型推导可以简单避免这个坑:

for (const auto& pa : m) {
    // ...
}

当然,用自动类型推导的时候,也可能引入一些坑。比如:

std::vector<bool> v2; 
v2.push_back(true);
v2.push_back(false);
auto b2 = v2[0];  // b2 是什么类型?

因为 std::vector<bool> 的特殊实现原因,变量 b2 不是一个 bool 类型,而是一个自定义的类。(无论你是否使用自动类型推导,都尽可能不要使用 std::vector<bool>。)

decltype

decltype 的作用是,告诉你一个表达式/变量/常量是什么类型。比如:

// 输出 i,表示 int
std::cout << typeid(decltype(1)).name() << std::endl; 

float f;
// 输出 f,表示 float
std::cout << typeid(decltype(f)).name() << std::endl;

unsigned a = 1;
unsigned long long b = 2;
// 输出 y,表示 unsigned long long
std::cout << typeid(decltype(a + b)).name() << std::endl;

typeid(T).name() 在不同的编译器下的输出可能不一样。本文在 Ubuntu 上使用 gcc 7.5 进行编译。

typeid(T).name() 的输出可以通过 c++filt 工具转换成实际可读的类型名称。

相比 auto,decltype 用得少很多。

举一个例子:

template<typename T, typename U>
??? Plus(T t, U u) 
  return t + u;
}

t + u 到底应该返回什么类型?

Plus(1, 2);  // 返回值类型应该是 int
Plus(1, 2.0);  // 返回值类型应该是 double 

使用 decltype 的 trailing return type 来解决这个问题:

template<typename T, typename U>
auto Plus(T t, U u) -> decltype(t + u) {
  return t + u;
}

C++ 14 进行了加强,可以省掉这条尾巴。

template<typename T, typename U>
auto Plus(T t, U u) {
  return t + u;
}

如果函数有多个 return 语句,需要保证它们返回的类型都是一样的才能成功编译。

// error: inconsistent deduction for
// auto return type: ‘int’ and then ‘double’
auto f(int i) {
  if (i == 1) {
    return 1;
  } else {
    return 2.0;
  }
}

decltype(auto)

使用 auto 需要自己手动说明是值类型还是引用类型。C++14 引入 decltype(auto) 来自动推导精确类型——其实 decltype(auto) 算是 decltype(expr) 的一个语法糖。

std::vector<std::string> v{"C++11", "C++14", "C++17"};

// v[0] 的返回值类型是 std::string&
// 但是 a 是 std::string
auto a = v[0]; 
// a 是 std::string&
auto& b = v[0];  
// C++11,我们可以这样确定精确类型
// c 是 std::string&
// 但是,如果 v[0] 变成一个复杂的表达式,代码写出来可能很难看懂
decltype(v[0]) c = v[0];  
// C++14 引入了 decltype(auto),可以自动推导出精确类型
// d 是 std::string&
decltype(auto) d = v[0];

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK