Effective Modern C++ 学习笔记1——类型推导

类型推导规则

在大多数情况下,模板与auto的类型推导规则一致,且规则很简单。

情况1. 没有加任何修饰

// 模板函数:
template <typename T>
f(T t) {...}

// auto声明:
auto x = ...;

这种情况下,参数是按值传递,形参t或者变量x都是一个副本,那么就需要去掉引用,且副本本身没必要加cv限定符(const、volatile,下文都不考虑volatile,只说const),也需要去掉。

const int& a = ...;

f(a);       // T 推导为 int
auto x = a; // x 的类型为 int

但需要注意,只能去掉变量本身的const,对于指针类型,所指对象的const修饰不能去掉。

const int* const a = ...;

f(a)        // T 推导为 const int*
auto x = a; // x 的类型为 const int*

其实不需要特别记忆,只要记住一点:在能编译通过的前提下,去掉没必要的const和&符号即可:

const int& a = ...;

// 以下语句能否编译通过?
const int& x = a;       // OK
int& x = a;             // 编译不通过,const int& 类型不可转为int&
const int x = a;        // OK
int x = a;              // OK,且去掉了多余的const和&符号
// 因此,auto x = a; 中,x类型推导为 int

const int* const b = ...;

// 以下语句能否编译通过?
int* const x = b;       // 编译不通过
const int* const x = b; // OK
const int* x = b;       // OK,且去除了多余的const
// 因此,auto x = b; 中,x类型推导为 const int*

情况2. 加了引用

也就是T&的形式:

// 模板函数:
template <typename T>
f(T& t) {...}

// auto声明:
auto& x = ...;

这种情况则需要保持原有的const,举例来看:

const int a = 1;

f(a); // T 推导为 const int,所以形参t类型为 const int&
auto& x = a; // x类型推导为 const int

保留const的原因也很好理解,因为这里是引用传递,丢掉const会导致可以修改原来的值,同样编译不通过:

const int a = 1;
int& x = a; // 编译不通过
const int& x = a; // OK

情况2.2 万能引用

如果加了两个引用符号 &&,则可以根据情况推导为普通的引用,或者右值引用,因此也叫做万能引用:

// 模板函数:
template <typename T>
f(T&& t) {...}   // &&表示万能引用

// auto声明:
auto&& x = ...;  // &&表示万能引用

// 举例:
int a = 1;
auto&& x = a;     // x的类型是 int&
auto&& x = 1;     // x的类型是 int&& 右值引用

其他情况

经过上面的分析,我们得出结论,对于模板类型的推导,其实只要能替换成普通函数,并去掉不必要的const和引用&,就可以得到推导结果。

书中特别提到了数组作为函数实参的特殊情况,但实际上这并不算什么特殊情况。我们先了解下普通函数,数组作为参数的情况。

在普通函数签名中,形参可以写成数组的形式,但实际上类型还是指针。二者最大的区别是:指针并不会包含数组长度信息。这也是为什么函数以数组作为参数时,一般还要再传入数组长度参数。

int getlen(char a[]) { // 等价于 int* a 或 int a[1]
    return sizeof(a);
}

int main() {
    char a[2];
    cout << sizeof(a); // 输出 2 即两个char所占的空间
    cout << getlen(a); // 输出 8 即int*指针所占的空间
}

其实,如果想要保留数组的长度信息,也有办法,那就是使用“数组的引用”作为函数参数:

int getlen(char (&a)[2]) { // a是数组char[2] 的引用类型
    return sizeof(a);  // 返回2
}

注意,形参中需指定长度,且需要与实参的数组长度一致。

而对于模板函数其实是一样的,可以使用数组作为参数,这样实际上的类型是指针;也可以使用“数组的引用”作为参数,保留长度信息。

template<typename T>
int f(T t) {
  return sizeof (t);
}

template<typename T>
int g(T& t) {
    return sizeof(t);
}

char a[2];
cout << f(a) << endl; // 输出 8, 即指针的长度,t被推导为char*类型
cout << g(a) << endl; // 输出 2, t被推导为 char(&)[2] 类型
auto x = a;   // x类型推导为 char*
auto& y = a;  // y类型推导为 char(&)[2]

仔细对比后发现,其实这和普通函数的规则完全一致,即:数组会退化为指针,数组引用则不会。

有趣的是,利用这一特性,我们可以写一个函数,获得数据长度:

template<typename T, std::size_t N>
constexpr std::size_t arraySize(T(&)[N]) {
  return N;
}

char a[2];
// 声明一个与数组a长度相同的array
std::array<char, arraySize(a)> arr{};

arraySize声明中的constexpr表示:返回的N是编译期常量。这样才可以用在array的声明中。

auto与模板唯一的区别

前面的每个例子中,都同时包含了模板与auto的类型推导,它们二者的推导结果都一摸一样。二者唯一的一点不同是:auto支持初始化表达式,而模板不支持:

template<typename T>
void f(T param);

f({1,2,3});          // 错误,无法推导
auto x = {1,2,3};    // OK,x类型推导为std::initializer_list<int>
auto y = {1,2,3.0};  // 错误,包含int和double,无法推导
int[] z = {1,2,3};   // OK

在上例中,模板与auto出现区别的根本原因在于,C++11中为了支持统一初始化,可以用这种语法进行声明;而函数参数则没有这样的语法。

当然,这样推导的结果总是std::initializer_list<T>的类型,往往不是我们想要的结果,所以还是尽量去避免这样使用。

auto使用的优劣

大多数时候,优先使用auto

使用auto的好处包括:

  • 简化一长串复杂的类型
  • 避免忘记初始化(使用auto未初始化会产生编译错误)
  • 类型变更时可以自适应
  • 能够为lambda表达式声明变量,且优于std::function,不需要堆内存
  • 避免由于类型写错导致额外的运行开销

关于最后一点,额外的运行开销,举例说明一下:

std::unordered_map<std::string, int> m;

// 不使用auto进行遍历,代码出现瑕疵:
for (const std::pair<std::string, int>& p : m) {
    //... 对p进行操作
}

// 使用auto:
for (const auto& p : m) {
    // ...
}

在for循环中,我们本意并不希望发生任何拷贝,因此使用了const引用的形式声明p,但很可惜,在代码运行中,仍然会发生拷贝。为什么呢?

原因是,m中元素正确类型是:std::pair<const std::string, int>,const不可以丢掉。由于这和代码中p的类型不匹配,编译器就会将其拷贝一份,并进行类型转换。所以,由于程序员对p类型的判断失误,导致运行期额外的性能开销。

而使用auto时,则永远会推断出正确的类型,可以完全避免这个问题。

auto使用的坑

有一种情况,使用auto无法得到我们想要的类型,这甚至可能会导致十分隐蔽的问题。例如:

std::vector<bool> GetVec() {
  return {true};
}

void HandleBool(bool b) {
  cout << b;
}

int main() {
  auto b = GetVec()[0]; // 这里auto如果换成bool,一切正常
  HandleBool(b);
}

本例中,我们将std::vector<bool>的operator[]操作结果赋值给b,期望变量b是bool类型,并在HandlerBool中进行打印。然而实际运行发现,打印的值往往不符合预期。

这是为什么?原因在于,b的类型并没有推断为bool,而是一个std::vector<bool>::reference类型的复杂对象,其中包含一个指针,指向vector<bool>中的某个位置。

之所以这样做,是因为vector内部针对bool类型做了一些优化。正常情况下,这个对象可以正确转化成bool类型。但是,在上例中,GetVec返回的vector在执行完这一行代码后已经销毁,b对象中的指针则成为野指针,在调用HandlerBool时,无法正确转化成bool类型。

在本例中,std::vector<bool>::reference类型我们称之为代理类,它可以模拟另一种类型。类似的,智能指针shared_ptr、unique_ptr也是代理类,它们可以模拟普通指针。但本例中的代理类更加隐蔽,我们难以察觉,才会出现上述问题。

所以,对于这种隐形代理类,需要根据情况避免使用auto来声明。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 230,247评论 6 543
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,520评论 3 429
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 178,362评论 0 383
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,805评论 1 317
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,541评论 6 412
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 55,896评论 1 328
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 43,887评论 3 447
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 43,062评论 0 290
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,608评论 1 336
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,356评论 3 358
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,555评论 1 374
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 39,077评论 5 364
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,769评论 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 35,175评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,489评论 1 295
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,289评论 3 400
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,516评论 2 379

推荐阅读更多精彩内容