文章目录

  • Effective.Modern.C++
  • 关键词翻译
    • Argument
    • Parameter
    • ParamType
    • expr 、expression
    • type deduction
    • trailing return type
  • Introduction 入门
    • 术语和约定
    • 报告bug并提出改进建议
  • 第一章:推导类型
    • 第1项:理解模板类型推导
      • 情况1:ParamType 是引用或指针,但不是通用引用
      • 情况3:ParamType 既不是指针也不是引用
      • 数组参数
      • 函数参数
    • 第2项:了解auto类型推导
    • 第3项:理解decltype
    • 第4项:知道如何查看推导出的类型。
      • IDE编辑器
      • 编译器的诊断程序
      • 运行时输出
  • 第二章:auto
    • 第5项:选择auto而不是explicit显式类型声明。
    • 第6项:当auto推断出不希望得到的类型时,请使用显式类型化初始化式惯用语。
  • 第三章:转向现代c++ (Moving to Modern C++)
    • 第7项:创建对象时区分圆括号(parentheses)和花括号{braces}
    • 第8项:优先使用nullptr而不是0和NULL。
    • 第9项:优先使用alias declarations而不是typedefs
    • 第10项:优先使用 有作用域的枚举而不是无作用域的枚举

Effective.Modern.C++

著作信息:

by Scott Meyers版权所有©2015 Scott Meyers。
保留所有权利。
在加拿大印刷。由O 'Reilly Media, Inc.出版,1005 Gravenstein Highway North,塞瓦斯托波尔,CA 95472

翻译原则: 一些我认为有争议的词句、我理解不了的,我会尽量保留英文原文,等确认清楚后再删除英文原文!!!

关键词翻译

Argument

实参

Parameter

形参

ParamType

参数类型,例如const int、char、int &等;注意再模板中ParamType != T,

例如: void f(const T & param), ParamType = const T &

出处:void f(ParamType param)

expr 、expression

函数调用参数表达式 (实参)

是不是可以翻译成实参?依据: In a function call, the expressions passed at the call site are the function’s arguments.

例如f(27)、f(x)

出处:f(expr); // call f with some expression

type deduction

类型推导

trailing return type

后置返回类型,介绍decltype 时用的到;

Introduction 入门

如果您是一位经验丰富的 C++ 程序员并且和我一样,那么您最初接触 C++11 时的想法是:“是的,是的,我明白了。 它是 C++,而且更是如此。” 但随着您了解的更多,您对更改的范围感到惊讶。 自动声明、基于范围的 for 循环、lambda 表达式和右值引用改变了 C++ 的面貌,更不用说新的并发特性了。 然后是惯用的变化。 0 和 typedefs 过时了,nullptr 和 alias 声明被加进来了。枚举现在应该是作用域的。 智能指针现在比内置指针更可取。 移动对象通常比复制它们更好。

关于 C++11 有很多东西要学,更不用说 C++14。

更重要的是,要有效利用新功能,还有很多东西要学。 如果您需要有关“现代” C++ 功能的基本信息,资源比比皆是,但如果您正在寻找有关如何使用这些功能来创建正确、高效、可维护和可移植的软件的指导,那么搜索就更具挑战性。 这就是本书的用武之地。它不是专门描述 C++11 和 C++14 的特性,而是介绍它们的有效应用。

书中的信息被分解成指导方针,称为项目。想了解类型推导的各种形式吗?或者知道何时(何时不)使用自动声明?你是否对为什么const成员函数应该是线程安全的,如何使用std::unique_ptr实现(Pimpl Idiom)即指向实现的指针,为什么应该避免lambda表达式中的默认捕获模式,或者std::atomic和volatile之间的区别感兴趣?答案都在这里。此外,它们是平台独立的、符合标准的答案。这是一本关于可移植c++的书。

本书中的项目是指导方针,而不是规则,因为指导方针有例外。
每个项目最重要的部分不是它提供的建议,而是建议背后的原理。
一旦你读到这些,你就可以确定你的项目的情况是否可以证明违反该项指导的合理性。
这本书的真正目的不是告诉你该做什么或避免做什么,而是传达对c++ 11和c++ 14中事情是如何工作的更深层次的理解。

术语和约定

​ 为了确保我们理解彼此,重要的是在一些术语上达成一致,具有讽刺意味的是,从“c++”开始。c++有四个官方版本,每个版本都以对应的ISO标准采用的年份命名:c++ 98、c++ 03、c++ 11和c++ 14。c++ 98和c++ 03只是在技术细节上有所不同,所以在本书中,我将两者都称为c++ 98。当我提到c++ 11时,我指的是c++ 11和c++ 14,因为c++ 14实际上是c++ 11的超集。当我写c++ 14时,我特别指的是c++ 14。如果我只是简单地提到c++,我是在做一个适用于所有语言版本的广义声明。

​ 因此,我可以说c++非常注重效率(对所有版本都是如此),c++ 98缺乏对并发性的支持(仅对c++ 98和c++ 03是如此),c++ 11支持lambda表达式(对c++ 11和c++ 14是如此),而c++ 14提供了通用函数返回类型推导(仅对c++ 14是如此)。

c++ 11最普遍的特性可能是 移动语义(move semantic),而 移动语义(move semantic)的基础是区分右值和左值表达式。
这是因为右值表示适合移动操作的对象,而左值通常不适合。
在概念上(虽然在实践中并不总是),右值对应从函数返回的临时对象,而左值对应的是可以通过名称或跟随指针或左值引用(int &)引用的对象。

确定表达式是否为左值的有用启发式方法是 询问是否可以获取其地址。
如果可以的话,通常是左值。如果你不能,它通常是右值。
这种启发式的一个很好的特性是,它帮助您记住表达式的类型与表达式是左值还是右值无关。
也就是说,给定一个T类型,你可以有T类型的左值,也可以有T类型的右值。在处理右值引用类型的形参时,特别重要的是要记住这一点,因为形参本身就是一个左值:

class Widget
{
public: Widget(Widget&& rhs);// RHS是一个左值,尽管它有一个右值引用类型…
};

在这里,在Widget的 移动构造函数 中获取rhs的地址是完全有效的,因此rhs是一个左值,尽管它的类型是一个右值引用。(同理,所有形参 都是左值。)

这段代码演示了我通常遵循的几个约定:

  1. 类名是Widget。每当需要引用任意用户定义的类型时,我都会使用Widget。除非我需要显示类的特定细节,否则我使用Widget而不声明它。

  2. 我使用参数名称rhs(“右手边”)。它是移动操作(即,移动构造函数和移动赋值操作符)和复制操作(即,复制构造函数和复制赋值操作符)的首选参数名。我也用它来求二元算符的右参数:

Matrix operator+(const Matrix& lhs, const Matrix& rhs);

我希望这并不奇怪,lhs代表“左手边”.

  1. 我对部分代码或部分注释应用特殊格式,以引起您的注意。
    在上面的Widget 移动构造函数中,我突出显示了rhs的声明以及注释中指出rhs是左值的部分。
    高亮显示的代码既不是天生的好,也不是天生的坏。
    这只是您应该特别注意的代码。

  2. 我用“…”来表示“其他代码可以放到这里”。
    这个窄省略号不同于c++ 11可变参数模板源代码中使用的宽省略号(“…”)。
    这听起来令人困惑,但事实并非如此。
    例如:

emplate<typename... Ts>    // these are C++ source code
void processVals(const Ts&... params)
{ // ellipses…  // this means "some code goes here"
}

processVals的声明表明,我在模板中声明类型形参 时使用了typename,但这只是个人偏好;关键字 class 也同样有效。
当我展示c++ Standard的代码摘录时,我使用class声明类型形参,因为这是标准的工作。


当一个对象用另一个相同类型的对象初始化时,新对象被称为初始化对象的副本,即使这个副本是通过移动构造函数创建的。
遗憾的是,c++中没有术语区分复制构造的复制对象和移动构造的复制对象:

void someFunc(Widget w);// someFunc的参数w是通过值传递的Widget wid; // wid is some Widget// 在这个someFunc的调用中,w是一个通过复制构造创建的wid的副本
someFunc(wid); someFunc(std::move(wid)); // 在这个SomeFunc调用中,w是一个通过move构造创建的wid的副本

右值的副本通常是移动构造的,而左值的副本通常是复制构造的。
这意味着,如果您只知道一个对象是另一个对象的副本,那么就不可能说出构造这个副本的代价有多大。
例如,在上面的代码中,如果不知道传递给someFunc的是右值还是左值,就无法说明创建参数w的开销有多大。
(您还必须知道移动和复制Widgets的成本。)

在函数调用中,在调用点传递的表达式是函数的实参。
实参用于初始化函数的形参。
在上面对someFunc的第一个调用中,实参是 wid。
在第二个调用中,实参是std::move(wid)。
在这两次调用中,形参都是w。实参和形参之间的区别很重要,因为形参是左值,但用于初始化形参的实参可以是右值或左值。
这在 完美转发(perfect forward)过程中尤其相关,在 完美转发(perfect forward)过程中,传递给一个函数的实参被传递给第二个函数,这样原始实参的右值或左值被保留。(完美转发详见第30项)

设计良好的函数是异常安全的,这意味着它们至少提供基本的异常安全保证(即基本保证)。
这样的函数保证调用者即使抛出异常,程序不变量仍然保持完整(也就是说,没有数据结构被破坏),没有资源被泄露。
提供强异常安全保证(即强保证)的函数向调用者保证,如果出现异常,程序的状态保持在调用之前的状态。

当我引用函数对象时,我通常指的是支持operator()成员函数的类型的对象。
换句话说,一个像函数一样的对象。
我偶尔会在更一般的意义上使用这个术语,表示可以使用非成员函数调用的语法调用的任何东西(即“函数名(参数)”)。
这个更广泛的定义不仅包括支持operator()的对象,还包括函数和类c函数指针。
(狭义的定义来自于c++ 98,广义的定义来自于c++ 11。)
通过添加成员函数指针进一步泛化,将产生所谓的可调用对象。
通常可以忽略这些细微的区别,简单地将函数对象和可调用对象看作是c++中可以使用某种函数调用语法调用的对象。

通过lambda表达式创建的函数对象被称为闭包。
很少有必要区分lambda表达式和它们创建的闭包,所以我经常将两者都称为lambdas。
类似地,我很少区分函数模板(即生成函数的模板)和模板函数(即从函数模板生成的函数)。
对于类模板和模板类也是如此。

c++中的许多东西可以同时声明和定义。
声明引入了名称和类型,但没有给出细节,比如存储在哪里或者东西是如何实现的:

extern int x; // 对象声明
class Widget; // 类声明
bool func(const Widget& w); // 函数声明
enum class Color; // scoped enum declaration,see Item 10)int x;  // object definition
class Widget
{ …// class definition
};bool func(const Widget& w) { return w.size() < 10; } // function definition
enum class Color { Yellow, Red, Blue }; // scoped enum definition

定义也可以作为声明,所以除非定义非常重要,否则我倾向于引用声明。

我将函数签名定义为其声明的一部分,用于指定参数和返回类型。
函数名和参数名不是签名的一部分。
在上面的例子中,func的签名是bool(const Widget&)。
函数声明中除形参和返回类型以外的元素(例如,noexcept或constexpr,如果存在)将被排除。(noexcept和constexpr见第14项和第15项)
“签名”的官方定义与我的略有不同,但对于这本书,我的定义更有用。
(官方定义有时会省略返回类型。)

新的c++标准通常保留了在旧标准下编写的代码的有效性,但有时标准化委员会会弃用某些特性。
这些特性被列入标准化死囚名单,并可能从未来的标准中删除。
对于使用已弃用的特性,编译器可能会发出警告,也可能不会,但是您应该尽量避免使用它们。
它们不仅会给将来的移植带来麻烦,而且通常还不如替代它们的特性。
例如,std::auto_ptr在c++ 11中已弃用,因为std::unique_ptr完成了相同的工作,只会更好。

有时,标准表示操作的结果是未定义的行为。
这意味着运行时行为是不可预测的,不用说,您应该想要避开这种不确定性。
具有未定义行为的操作示例包括使用方括号(“[]”)在std::vector范围之外建立索引,取消对未初始化迭代器的引用,或进行数据竞争(即多线程互斥,有两个或多个线程,其中至少有一个是写入器,同时访问相同的内存位置)。

我调用内置类型指针,比如那些从新的原始指针返回的指针。
与原始指针相反的是智能指针。
智能指针通常会重载指针解引用操作符(operator->和operator*),不过Item 20解释说std::weak_ptr是个例外。

在源代码注释中,我有时会将“构造函数”缩写为ctor,将“析构函数”缩写为dtor。

报告bug并提出改进建议

我已经尽我所能让这本书充满清晰、准确、有用的信息,但肯定有办法让它变得更好。
如果你发现任何类型的错误(技术性的、说明性的、语法的、排版的,等等),或者如果你对如何改进这本书有建议,请给我发邮件:emc++@aristeia.com。
新的打印给了我修改Effective Modern c++的机会,我不能解决我不知道的问题!


第一章:推导类型

c++ 98只有一组类型推导规则:函数模板规则。
c++ 11稍微修改了一下该规则集,并增加了两个规则集,一个用于auto,一个用于decltype。
然后,c++ 14扩展了auto和decltype可以使用的上下文。
类型推导的日益广泛的应用,使您 可以从拼写出“明显的或冗余的类型” 的强制规则中解脱出来。
它使c++软件的适应性更强,因为在源代码中的某一点更改类型会自动通过类型推导传播到其他位置。
但是,它会使呈现的代码变得更难理解,因为编译器推导出的类型可能不像您希望的那样明显。

如果没有对类型推导如何运作的深刻理解,现代 C++ 中的有效编程几乎是不可能的。 发生类型推导的上下文太多了:在调用函数模板时,在 auto 出现的大多数情况下,在 decltype 表达式中,以及从 C++14 开始,使用神秘的 decltype(auto) 构造。

本章提供了每个c++开发人员都需要的关于类型推导的信息。
它解释了模板类型推导是如何工作的,“自动类型” 如何在此基础上构建,以及decltype如何按照自己的方式进行。
它甚至解释了如何强制编译器使其 类型推导 的结果可见,从而使您能够确保编译器推导出您希望它们推导的类型。

第1项:理解模板类型推导

​ 当一个复杂系统的用户不知道它是如何工作的,但对它所做的事情感到满意时,这就说明了系统设计的重要性。通过这种方法,C++中的模板类型推导取得了巨大的成功。数百万程序员已将参数传递给模板函数,并取得了完全令人满意的结果,尽管其中许多程序员很难给出关于这些函数使用的类型是如何推导的最模糊的描述。

​ 如果那群人包括你,我有好消息和坏消息。好消息是,模板的类型推断是现代C++最引人注目的功能之一的基础:auto。如果您对c++ 98为模板推导类型感到满意,那么您也会对c++ 11为auto推导类型感到满意。坏消息是,当模板类型推导规则应用于auto上下文中时,它们有时看起来不如应用于模板时直观。因此,真正理解自动构建所依赖的模板类型推导的各个方面是很重要的。这个项目涵盖了你需要知道的。

如果您愿意忽略一些伪代码,我们可以将函数模板想象为如下所示:

template<typename T>
void f(ParamType param);  // !!!注意,后文经常提到ParamType

调用可以是这样的:

f(expr); // call f with some expression

在编译过程中,编译器使用expr推导出两种类型:一种用于T,另一种用于ParamType。这些类型通常是不同的,因为ParamType通常包含修饰符,例如const或引用限定符。例如,如果模板像这样声明,

template<typename T>
void f(const T& param);// ParamType is const T&

像这样调用时,

int x = 0;
f(x);// call f with an int

T被推导为int,但ParamType被推导为const int&。

我们很自然地认为,为T推导出的类型与传递给函数的实参类型相同,即T是expr的类型。
在上面的例子中,情况就是这样:x是一个int,那么T被推导为int。但情况并不总是这样。
为T推导出的类型不仅依赖于expr 这个 调用参数表达式 的类型,还依赖于ParamType的类型构成。
有三种情况:

  • ParamType 是指针或引用类型,但不是通用引用(即: &&因为它可以解释为很多种引用)。
    (通用引用见第24项。此时,您只需要知道它们的存在,并且它们与左值引用或右值引用不同。)
  • ParamType 是一个通用引用(即: &&)。
  • ParamType 既不是指针也不是引用。

因此,我们有三种类型的推导场景要检查。每一个都将基于模板的一般形式和对它的调用:

template<typename T>
void f(ParamType param);
f(expr); //  从expr推导T和ParamType

情况1:ParamType 是引用或指针,但不是通用引用

​ 最简单的情况是,ParamType是引用类型或指针类型,但不是通用引用。
在这种情况下,类型推导是这样的:

  1. 如果expr 的类型是引用,忽略引用部分。
  2. 然后将expr的类型与ParamType进行模式匹配,以确定T。

例如,如果这是我们的模板,

 template<typename T>
void f(T& param); // param is a reference

我们有这些变量声明,

int x = 27;  // x is an int
const int cx = x;  // cx is a const int
const int& rx = x;  // rx is a reference to x as a const int

在各种调用中,param和T的推导类型如下所示:

f(x);    // T is int, param's type is int&
f(cx); // T is const int,param's type is const int&
f(rx); // T is const int,param's type is const int&, FIX:这个引用来源于 param's type(T& param),而不是rx定义时的引用;

在第二次和第三次调用中,请注意,因为cx和rx指定为const属性,所以T被推导为const int,从而产生const int&形参类型。
这对来调用者 来说很重要。
当它们将const对象传递给引用形参时,它们期望该对象保持不可修改,即形参是指向const的引用。
这就是为什么将const对象传递给带T&形参的模板是安全的:该对象的const属性成为 为T推导的类型的一部分。

在第三个例子中,请注意,尽管rx的类型是引用,但T被推导为非引用。
这是因为rx的引用在类型推导过程中被忽略。

​ 这些示例都显示了左值引用形参,但类型推导与右值引用形参的工作方式完全相同。当然,只有右值参数 可以传递给右值引用形参,但这一限制与类型推导无关。

如果我们把f的形参类型从T&改为const T&,事情会有一点变化,但不会有什么特别的变化。cx和rx的const属性 仍然受到推崇,但因为我们现在假设param是 reference-to-const,所以不再需要将const推导为T的一部分:

template<typename T>
void f(const T& param); // param is now a ref-to-constint x = 27;          // as before
const int cx = x;  // as before
const int& rx = x; // as beforef(x);   // T is int, param's type is const int&
f(cx);  // T is int, param's type is const int&
f(rx);  // T is int, param's type is const int&

和以前一样,rx的引用在类型推导期间被忽略。
如果param是一个指针(或指向const的指针)而不是引用,事情本质上将以相同的方式运行:

template<typename T>
void f(T* param);   // param is now a pointer int x = 27;      // as before
const int *px = &x;  // px is a ptr to x as a const intf(&x); // T is int, param's type is int*
f(px);// T is const int, param's type is const int*

到目前为止,您可能会发现自己在打呵欠和瞌睡,因为c++的类型推导规则对于引用和指针参数的工作非常自然,看到它们以书面形式出现实在是太无聊了。
一切只是明显!
这正是类型推导系统所需要的。

###情况2:ParamType 是一个通用引用

对于采用通用引用形参的模板,情况就不那么明显了。
这样的形参声明类似于右值引用(也就是说,在接受类型形参T的函数模板中,通用引用的声明类型是T&&),但当传入左值实参时,它们的行为不同。
(第24项) 讲述了完整的故事,但这是标题版本:

  • 如果 expr 是左值,T和param都被推断为左值引用。这是双重不同寻常。
    首先,这是模板类型推导中唯一将T推导为引用的情况。
    第二,虽然param是使用右值引用的语法声明的,但它的推导类型是左值引用。
  • 如果 expr 是右值,则应使用“正常” (即情况1) 规则。

例如:

template<typename T>
void f(T&& param); // param is now a universal reference
int x = 27;        // as before
const int cx = x;  // as before
const int& rx = x; // as beforef(x); // x is lvalue, so T is int&, // param's type is also int&f(cx); // cx is lvalue, so T is const int&, // param's type is also const int&f(rx); // rx is lvalue, so T is const int&, // param's type is also const int&f(27); // 27 is rvalue, so T is int, // param's type is therefore int&&
// 个人验证结果template<typename T>
void f(T &&param)   // param is still passed by value
{cout << "int: " << boolalpha << std::is_same<T, int>::value << endl;cout << "int&: " << boolalpha << std::is_same<T, int&>::value << endl;cout << "const int&: " << boolalpha << std::is_same<T, const int&>::value << endl;cout << "int&&: " << boolalpha << std::is_same<T, int&&>::value << endl;cout << "param: " << param << endl; // Fun with pointerstypecout << endl;
}
int x = 27;        // as before
const int cx = x;  // as before
const int& rx = x; // as before
int main(void)
{f(x); // x is lvalue, so T is int&, // param's type is also int&f(cx); // cx is lvalue, so T is const int&, // param's type is also const int&f(rx); // rx is lvalue, so T is const int&, // param's type is also const int&f(27); // 27 is rvalue, so T is int, // param's type is therefore int&&return 1;
}// 输出:// int: false
// int& : true
// const int& : false
// int&& : false
// param : 27
//
// int : false
// int& : false
// const int& : true
// int&& : false
// param : 27
//
// int : false
// int& : false
// const int& : true
// int&& : false
// param : 27
//
// int : true
// int& : false
// const int& : false
// int&& : false
// param : 27

(第24项)解释了为什么这些例子会以这样的方式出现。
这里的重点是,通用引用形参 的类型推导规则不同于左值引用或右值引用形参的类型推导规则。
特别是,当使用全域引用时,类型推导 可以区分左值实参和右值实参。
这在非通用引用中是不会发生的。

情况3:ParamType 既不是指针也不是引用

当ParamType既不是指针也不是引用时,我们处理的是按值传递:

template<typename T>
void f(T param); // param is now passed by value,值传递

这意味着参数将是传递内容的副本——一个全新的对象。
param将是一个新对象的事实,促使控制如何从 函数调用表达式 推导出T的规则:

  • 和前面一样,如果 函数调用表达式 的类型是引用,则忽略引用部分。
  • 如果在忽略了函数调用表达式 的引用性之后,表达式是const,也忽略它。如果它是volatile 关键字(和 const 对应),也忽略它。(volatile对象不常见。它们通常只用于实现设备驱动程序。具体请参见第40项。)
template<typename T>
void f(T param); // param is now passed by valueint x = 27;  // as before
const int cx = x;// as before
const int& rx = x; // as before
f(x);  // T's and param's types are both int
f(cx); // T's and param's types are again both int
f(rx);  // T's and param's types are still both int
// 实测代码template<typename T>
void f(T param) // param is still passed by value
{cout << "int: " << boolalpha << std::is_same<T, int>::value << endl;cout << "int&: " << boolalpha << std::is_same<T, int&>::value << endl;cout << "const int&: " << boolalpha << std::is_same<T, const int&>::value << endl;cout << "param: " << param << endl; // Fun with pointerstypecout << endl;
}
int x = 27;  // as before
const int cx = x;// as before
const int& rx = x; // as before
int main(void)
{f(x);  // T's and param's types are both int f(cx); // T's and param's types are again both intf(rx);  // T's and param's types are still both intreturn 1;
}
//
// int: true
// int& : false
// const int& : false
// param : 27
//
// int : true
// int& : false
// const int& : false
// param : 27
//
// int : true
// int& : false
// const int& : false
// param : 27

注意,即使cx和rx定义为const值,模板函数参数Param 也不会是const。这是有意义的。
Param是一个完全独立于cx和rx的对象,是cx或rx的副本。
cx和rx不能被修改的事实并不能说明param是否可以被修改。
这就是为什么在为param推导类型时忽略 expr 的常量性(以及volatile,如果有的话):仅仅因为 expr 不能修改并不意味着它的副本也不能修改。

​ 必须认识到,只有按值传递的形参才会忽略const(和volatile)。如我们所见,对于指向const的引用形参 或 指向const的指针形参,expr的常量性在类型推导过程中保持不变。
​ 但是考虑一下,expr是指向const对象的const指针,并且expr被传递给一个byvalue值传递的形参:

template<typename T>
void f(T param);    // param is still passed by valueconst char* const ptr = "Fun with pointers"; // ptr is const pointer to const object f(ptr); // pass arg of type const char * const
// 实测代码template<typename T>
void f(T param) // param is still passed by value
{cout << "int: " << boolalpha << std::is_same<T, int>::value << endl;cout << "const char *: " << boolalpha << std::is_same<T, const char *>::value << endl;cout << "param: " << param << endl; cout << endl;
}
const char* const ptr = "Fun with pointers"; // ptr is const
int main(void)
{f(ptr); // pass arg of type const char * constreturn 1;
}
// int: false
// const char* : true
// param : Fun with pointers

在这里,星号右边的const声明表明了ptr指针为const: ptr,不能修改指向其他位置,也不能设置为null。
(星号左侧的const表示ptr指针指向的字符串是const,因此不能修改。)
当ptr被传递给f函数时,组成指针的二进制位被复制到param中。
因此,指针本身(ptr)将通过值传递。
根据值传递形参的类型推导规则,ptr的const属性(星号右边的const)被忽略,参数param推导出的类型为const char*,即指向const字符串的可修改指针。
ptr所指向对象的const属性在类型推导过程中保留,但是在复制ptr来创建新指针param时,ptr指针本身的常量(星号右边的const)会被忽略。


数组参数

前边几乎涵盖了主流模板类型推导的全部内容,但还有一个小众案例值得了解。
数组类型不同于指针类型,尽管它们有时看起来是可互换的。
造成这种错觉的一个主要原因是,在许多上下文中,数组衰减为指向其第一个元素的指针。
这种衰减允许了以下这样的代码编译通过:

// name's type is const char[13]
const char name[] = "J. P. Briggs"; const char * ptrToName = name; // array decays to pointer

在这里,const char* ptrToName 被初始化为name,它是一个const char[13]。
这些类型(const char*和const char[13])并不相同,但由于数组到指针的衰减规则,代码可以编译。
但是,如果一个数组被传递给一个接受按值参数的模板呢?
然后会发生什么呢?

template<typename T>
void f(T param);  // template with by-value parameterconst char name[] = "J. P. Briggs";
f(name); // what types are deduced for T and param?

我们首先观察到,没有一个函数参数是数组。是的,语法是合法的,

void myFunc(int param[]);

但数组声明被视为指针声明,这意味着myFunc可以等价地这样声明:

void myFunc(int* param); // same function as above

数组和指针形参的等价性有点像从c++基础的C根中派生出来的,它造成了数组和指针类型是相同的错觉。

因为数组形参声明被当作指针形参来处理,通过值传递给模板函数的数组类型被推导为指针类型。
这意味着在对模板f的调用中,其类型形参T被推导为const char*:

const char name[] = "J. P. Briggs";
f(name); // name is array, but T deduced as const char*

But now comes a curve ball. Although functions can’t declare parameters that are truly arrays, they can declare parameters that are references to arrays! So if we modify the template f to take its argument by reference,

但现在出现了一个曲线球。
虽然函数不能声明真正的数组形参,但它们可以声明引用数组的形参!
所以如果我们修改模板函数f来引用它的参数,

template<typename T>
void f(T& param);  // 带有引用形参的模板

我们传递一个数组给它,

const char name[] = "J. P. Briggs";
f(name); // pass array to f
// 自测代码template<typename T>
void f(T& param) { cout << sizeof(param) << endl; }  // 带有引用形参的模const int arr[] = { 1, 2, 3 };
int main(void){f(arr); // 12  ,const int (&)[3]return 1;
}

为T推导的类型就是数组的实际类型!
该类型包含数组的大小,因此在本例中,T被推导为const char[13],而f的形参(对该数组的引用)的类型为const char(&)[13]。
是的,语法看起来有毒,但知道它会让你在那些关心你的人那里得到更多的分数。

// 自测代码
// 模板可以把T推导成const char[13],函数不行类型是固定的
const int arr[] = { 1, 2, 3 };
void func(const int& i) {}
int main(void)
{func(arr); // error C2664: “void func(const int &)”: 无法将参数 1 从“const int [3]”转换为“const int &”return 1;
}

有趣的是,声明数组引用的能力允许创建一个模板来推断数组中包含的元素的数量:

// return size of an array as a compile-time constant. (The array parameter has no name, because we care only about the number of elements it contains.)//constexpr 应用于变量:当任何代码尝试修改值时,都会引发编译器错误。与 const 不同,constexpr 也可以应用于函数和类构造函数.
// const 变量的初始化可以推迟到运行时。constexpr 变量必须在编译时初始化
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept
{return N;
}

正如(第15项)所解释的,声明此函数constexpr使其结果在编译期间有效(const 变量的初始化可以推迟到运行时。constexpr 变量必须在编译时初始化)。
这样就可以声明一个具有相同数量元素的数组,而第二个数组的大小是通过带大括号的初始化式计算的:

// keyVals has 7 elements
int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 }; // so does mappedVals
int mappedVals[arraySize(keyVals)];  // 编译期必须知道数组大小

当然,作为一个现代c++开发人员,你自然会更喜欢std::array而不是内置数组:

std::array<int, arraySize(keyVals)> mappedVals;
// mappedVals'  size is 7

至于arraySize被声明为noexcept,这是为了帮助编译器生成更好的代码。
具体操作请参见(第14项)。


函数参数

数组并不是 C++ 中唯一可以衰减为指针的东西。 函数类型可以衰减为函数指针,我们讨论过的有关数组类型推导的所有内容都适用于函数的类型推导及其衰减为函数指针。 因此:

// someFunc is a function; type is void(int, double)
void someFunc(int, double); template<typename T>
void f1(T param); // in f1, param passed by valuetemplate<typename T>
void f2(T& param);  // in f2, param passed by ref// param deduced as ptr-to-func;type is void (*)(int, double)
f1(someFunc); // param deduced as ref-to-func;type is void (&)(int, double)
f2(someFunc);

这在实践中几乎没有什么不同,但是如果您想了解数组到指针的衰减,那么您也可以了解函数到指针的衰减。

这样您就有了模板类型推导的自动相关规则。
我在一开始就说过,它们非常简单,在大多数情况下,它们确实如此。
然而,在为通用引用推导类型时给予左值的特殊处理使问题变得更加复杂,而数组和函数的衰变到指针的规则则使问题变得更加复杂。
有时,您只是想获取编译器并要求:“告诉我您推导的是什么类型!”
当发生这种情况时,请参阅 (第4项),因为它致力于诱导编译器完成这一任务。

要记住的事情:

  • 在模板类型推导期间,引用实参 被视为非引用,即它们的引用性被忽略。
  • 在为通用引用形参推导类型时,左值实参 得到特殊处理。
  • 当对值传递by-value 形参进行类型推导时,const、volatile 修饰的实参被视为 non-const和non-volatile 实参。
  • 在模板类型推导过程中,数组或函数名参数会衰变为指针,除非它们用于初始化引用(即: paramtype & param)。

第2项:了解auto类型推导

​ 如果你已经阅读了关于模板类型推导的 第1项,那么你几乎已经了解了关于auto类型推导的所有知识,因为除了一个奇怪的例外,auto类型推导就是模板类型推导。 但怎么可能呢? 模板类型推导涉及模板和函数和参数,但 auto 不处理这些事情。

这是真的,但没关系。模板类型推导和auto类型推导之间存在直接映射。从一个到另一个实际上有一个算法转换。在第1项中,使用这个通用函数模板解释了模板类型推导

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

和这个普遍的呼叫:

f(expr); // call f with some expression

在对f()的调用中,编译器使用expr推导T和ParamType的类型。

当使用auto声明变量时,auto在模板中扮演T的角色,变量的类型说明符充当ParamType,所以考虑下面这个例子:

auto x = 27;

这里,x的类型说明符本身就是auto。另一方面,在这个表达式中,

const auto cx = x;

类型说明符是const auto。在这里

const auto& rx = x;

类型说明符是const auto&。
为了推导这些例子中x、cx和rx的类型,编译器的行为就好像每个声明都有一个模板,并使用相应的初始化表达式调用该模板:

template<typename T>   // 推导x类型的概念模板
void func_for_x(T param);   func_for_x(27); // 概念性调用:param推导的类型是x的类型template<typename T>  // 推导cx类型的概念模板
void func_for_cx(const T param);func_for_cx(x);     //概念性调用:param推导的类型是cx的类型template<typename T>  // 推导cx类型的概念模板
void func_for_rx(const T& param);func_for_rx(x);        //概念性调用:param推导的类型是rx的类型

正如我所说的,为auto推断类型与为模板推断类型是一样的,只有一个例外(我们将很快讨论)。第1项根据一般函数模板中param的类型说明符ParamType的特征,将模板类型推导分为三种情况。在使用auto的变量声明中,类型说明符会取代ParamType,因此也有三种情况:

  • 情况1:类型说明符是指针或引用,但不是通用引用。
  • 情况2:类型说明符是一个通用引用。
  • 情况3:类型说明符既不是指针也不是引用。

我们已经看到了 情况1 和 情况3 的例子:

auto x = 27; // case 3 (x is neither ptr nor reference) const auto cx = x; // case 3 (cx isn't either)const auto& rx = x; // case 1 (rx is a non-universal ref.)

情况2如你所愿:

// x is int and lvalue, so uref1's type is int& 左值引用
auto&& uref1 = x;  // cx is const int and lvalue,so uref2's type is const int&
auto&& uref2 = cx; // 27 is int and rvalue, so uref3's type is int&& 右值引用
auto&& uref3 = 27;

第1项最后讨论了数组和函数名如何衰减为非引用类型说明符的指针。
这在自动类型推导中也会发生:

// name's type is const char[13]
const char name[] =    "R. N. Briggs";   auto arr1 = name;  // arr1's type is const char*auto& arr2 = name; // arr2's type is const char (&)[13]// someFunc is a function;  type is void(int, double)
void someFunc(int, double);// func1's type is  void (*)(int, double)
auto func1 = someFunc; // func2's type is void (&)(int, double)
auto& func2 = someFunc;

如您所见,自动类型推导的工作原理与模板类型推导类似。
它们本质上是同一枚硬币的两面。

Except for the one way they differ. We’ll start with the observation that if you want to declare an int with an initial value of 27, C++98 gives you two syntactic choices:

除了一点不同。我们将从观察开始,如果你想声明一个初始值为27的int类型,c++ 98给你两种语法选择:

int x1 = 27;
int x2(27);

C++11,通过它对统一初始化的支持,增加了这些:

int x3 = { 27 };
int x4{ 27 };

总共有四种语法,但只有一个结果:值为27的int。

但是正如第5项所解释的那样,使用auto来声明变量而不是固定类型有很多优点,所以在上面的变量声明中用auto来替换int会更好。
直接的文本替换产生以下代码:

auto x1 = 27;
auto x2(27);
auto x3 = { 27 };
auto x4{ 27 };

这些声明都可以编译,但它们的含义与它们替换的含义不同。 前两条语句确实声明了一个值为 27 的 int 类型的变量。然而,后两条声明了一个 std::initial izer_list 类型的变量,其中包含一个值为 27 的元素!

auto x1 = 27; // type is int, value is 27
auto x2(27); // ditto// type is std::initializer_list<int>,  value is { 27 }
auto x3 = { 27 };
auto x4{ 27 };  // ditto

这是由于auto的特殊类型推导规则造成的。
当自动声明的变量的初始化式用大括号括起来时,推导出的类型是std::initializer_list。如果不能推导出这样的类型(例如,因为带大括号的初始化式中的值是不同的类型),代码将被拒绝,编译不过:

auto x5 = { 1, 2, 3.0 };
// error C3535: 无法推导“auto”的类型(依据“initializer list”)
// error C2398: 元素“3”: 从“double”转换到“int”需要收缩转换

正如注释所指出的,在这种情况下类型推导将失败,但重要的是要认识到实际上发生了两种类型推导。
一种源于auto的使用:必须推断x5的类型。因为x5的初始化式是用大括号括起来的,所以必须将x5推导为std::initializer_list。
但是std::initial izer_list是一个模板
对于某些类型T,实例化是std::initializer_list,这意味着T的类型也必须推导出来。这种推导属于这里发生的第二种类型推导的范围:模板类型推导。
在本例中,这种推断失败了,因为带大括号的初始化式中的值没有单一类型。

​ 带大括号的初始化式的处理是auto类型推导和模板类型推导的唯一不同之处。当使用带大括号的初始化式初始化一个auto声明的变量时,推导出的类型是std::initializer_list的实例化。但是, 但是如果对应的模板传递了相同的初始化式,类型推导失败,代码被拒绝:

auto x = { 11, 23, 9 }; // x's type is// std::initializer_list<int>template<typename T> //模板参数声明等价于x的声明
void f(T param);f({ 11, 23, 9 });  // error! can't deduce type for T
// error C2672: “f”: 未找到匹配的重载函数
// error C2783: “void f(T)”: 未能为“T”推导 模板 参数

但是,如果你在模板中指定参数是std::initializer_list,对于某个未知的T,模板类型推导将推导出T是什么:

template<typename T>
void f(std::initializer_list<T> initList);f({ 11, 23, 9 });
// T deduced as int, and initList's type is std::initializer_list<int>

所以 auto 和模板类型推导之间唯一真正的区别是 auto 假定大括号初始化器表示 std::initializer_list,但模板类型推导不(模板没办法先帮{ 11, 23, 9 } 推导成std::initializer_list,导致T无法推导出来)。

您可能想知道,为什么auto类型推导对于带大括号的初始化式有特殊的规则,而模板类型推导却没有。我自己也在想。唉,我还没能找到一个令人信服的解释。
但规则就是规则,这意味着你必须记住,如果你使用auto声明一个变量,然后用带大括号的初始化式初始化它,推导出来的类型将始终是std::initializer_list。
如果您接受统一初始化的理念——将初始化值括在大括号内,那么记住这一点尤为重要。
c++ 11编程中的一个经典错误是,当您打算声明其他变量时,意外地声明了一个std::initializer_list变量。
这个陷阱是一些开发人员只有在必要时才在初始化式周围加上大括号的原因之一。
(当你必须在第7项中讨论时。)

对于c++ 11,这就是全部的故事,但是对于c++ 14,故事还在继续。
c++ 14允许auto指明函数的返回类型应该被推导出来(参见第3项),而c++ 14的lambdas可以在形参声明中使用auto。
但是,auto的这些使用 采用模板类型推导规则,而不是auto类型推导。
因此,一个返回带大括号初始化式的自动返回类型的函数是无法编译的:

auto createInitList()
{// error: can't deduce type for { 1, 2, 3 }return { 1, 2, 3 };
}

当auto在c++ 14 lambda的参数类型规范中使用时也是一样的:

std::vector<int> v;
…
auto resetV = [&v](const auto& newValue) { v = newValue; };
…
// error! can't deduce type for { 1, 2, 3 }
resetV({ 1, 2, 3 });

记住这些:

  • 自动类型推导通常与模板类型推导相同,但自动类型推导假设是带大括号的初始化式则表示std::initial izer_list,而模板类型推导则不然。
  • 函数返回类型或 lambda 参数中的 auto 意味着模板类型推导,而不是自动类型推导。

第3项:理解decltype

​ decltype 是一个奇怪的生物。 给定名称或表达式,decltype 会告诉您名称或表达式的类型。 通常,它告诉你的正是你所预测的。 然而,有时它提供的结果会让您摸不着头脑,转向参考作品或在线问答网站寻求启示。

我们将从典型的案例开始——那些没有意外的案例。
与使用模板和auto进行类型推导时所发生的情况相反(请参阅第1项和第2项),decltype 通常会模仿你给它的名称或表达式的确切类型:

// 打印方法: cout << typeid(decltype(w)).name() << endl;const int i = 0; // decltype(i) is const int
bool f(const Widget& w);// decltype(w) is const Widget& // decltype(f) is bool(const Widget&)
struct Point
{ int x, y; // decltype(Point::x) is int // decltype(Point::y) is int
}; Widget w; // decltype(w) is Widgetif (f(w))  // decltype(f(w)) is bool… template<typename T>   // simplified version of std::vector
class vector {public: …T& operator[](std::size_t index); …
};vector<int> v; // decltype(v) is vector<int>
…
if (v[0] == 0)    // decltype(v[0]) is int&…

看到了吗?没有惊喜。

在c++ 11中,decltype的主要用途可能是声明函数模板,其中函数的返回类型取决于它的形参类型
例如,假设我们想编写一个函数,该函数接受一个通过方括号(即使用"[]")和索引支持索引的容器,然后在返回索引操作结果之前验证用户。函数的返回类型应该与索引操作返回的类型相同。

opertor[]在T类型对象的容器上通常返回一个T&。
例如,对于std::deque就是这样,对于std::vector几乎总是这样。
然而,对于std::vector,opertor[]不返回bool&。相反,它返回一个全新的对象。第6项将探讨这种情况的原因和方式,但这里重要的是,容器的opertor[]返回的类型取决于容器。

​ decltype 可以很容易地表达出来。 这是我们想要编写的模板的第一个剪辑,展示了使用 decltype 来计算返回类型。 模板需要一些细化,但我们暂时推迟:

template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i) -> decltype(c[i])
{authenticateUser(); return c[i];
}

在函数名之前使用auto与类型推导无关。这是c++ 11的后置返回类型(trailing return type)语法,也就是说,该语法允许返回类型在参数列表之后声明(在" -> "之后)。
后置返回类型(trailing return type)的优点是函数的参数可以用于返回类型的指定。例如,在authAndAccess中,我们使用c和i指定返回类型。如果按照传统方式将返回类型放在函数名之前,那么c和i将不可用,因为它们还没有被声明。

使用这个声明,authAndAccess 返回任何类型的 operator[] 在应用于传入的容器时的返回类型(c[i]),这正是我们想要的。

c++ 11允许推导单语句lambdas的返回类型,而c++ 14将其扩展到所有lambdas和所有函数,包括那些具有多个语句的函数。
在authAndAccess的情况下,这意味着在c++ 14中我们可以省略后面的返回类型,只留下前面的auto。
对于这种形式的声明,auto确实意味着将进行类型推导。
特别地,它意味着编译器将从函数的实现中推断出函数的返回类型:

template<typename Container, typename Index>   // C++14;
auto authAndAccess(Container& c, Index i) { // 不完全对authenticateUser(); return c[i]; // return type deduced from c[i]
}

第2项解释了对于具有自动返回类型规范的函数,编译器使用模板类型推导。
在这种情况下,这是有问题的。
正如我们所讨论的,对于大多数t容器,operator[]返回一个T&,但是第1项解释说,在模板类型推导期间,初始化表达式的引用被忽略。
考虑一下这对这个客户端代码意味着什么:

std::deque<int> d;
…
authAndAccess(d, 5) = 10; //编译出错  error C2106: “=”: 左操作数必须为左值;原因就是return 忽略了c[i]的引用属性,变成值传递了
// authenticate user, return d[5], then assign 10 to it; this won't compile!

这里,d[5]返回一个int&,但是authAndAccess的auto返回类型推导将去掉引用,从而产生int的返回类型。
作为函数返回值的int是一个右值,因此上面的代码试图将10赋给右值int。
这在c++中是禁止的,所以代码无法编译。

为了让authAndAccess按照我们想要的方式工作,我们需要对它的返回类型使用decltype类型推导,也就是说,指定authAndAccess应该返回与表达式c[i]返回的完全相同的类型。
c++的守护者们预见到在推断类型的某些情况下需要使用decltype类型推导规则,因此在c++ 14中通过decltype(auto)说明符使这成为可能。
最初看起来矛盾的(decltype和auto?)实际上非常有意义:auto指定要推导的类型,而decltype表示在推导过程中应该使用decltype规则。
因此,我们可以这样编写authAndAccess:

// c++ 14;可以工作,但仍然需要改进
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index i)
{authenticateUser(); return c[i];
}

现在authAndAccess会真正返回c[i]的 返回值。
特别地,对于c[i]返回一个T&的常见情况,authAndAccess也将返回一个T&,而在c[i]返回一个对象的不常见情况下,authAndAccess也将返回一个对象。

decltype(auto) 的使用不限于函数返回类型。 当您想将 decltype 类型推导规则应用于初始化表达式时,也可以方便地声明变量:

Widget w;
const Widget& cw = w;
// auto type deduction: myWidget1's type is Widget
auto myWidget1 = cw;// decltype type deduction:myWidget2's type is const Widget&
decltype(auto) myWidget2 = cw;

但我知道有两件事困扰着你。一是对authAndAccess的改进,我提到过,但还没有描述。让我们现在来解决这个问题。
再看一下c++ 14版本的authAndAccess的声明:

template<typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index i);
//要结合上边的实现部分,理解下边的话

该容器通过lvalue-reference-to-non-const传递,因为返回对容器元素的引用( 对于大多数t容器,operator[]返回一个T&)允许客户端修改该容器。
但这意味着不可能将右值容器传递给该函数。
右值不能绑定到左值引用(除非它们是lvalue-references-to-const 左值常量引用,这里不是这样)。

诚然,向authAndAccess传递右值容器是一种边缘情况。
右值容器是一个临时对象,通常会在包含对authAndAccess调用的语句结束时销毁,这意味着对该容器中某个元素的引用(通常是authAndAccess返回的内容)会在创建它的语句结束时悬空。
尽管如此,向authAndAccess传递一个临时对象还是有意义的。
客户端可能只是想在临时容器中复制一个元素,例如:

// factory function
std::deque<std::string> makeStringDeque(); // 复制makeStringDeque返回的deque容器的第5个元素
auto s = authAndAccess(makeStringDeque(), 5);// 编译报错: “decltype(auto) authAndAccess<std::deque<std::string,std::allocator<std::string>>,int>(Container &,Index)”: 无法将参数 1 从“std::deque<std::string,std::allocator<std::string>>”转换为“Container &”,非常量引用只能绑定到左值

支持这样的使用意味着我们需要修改authAndAccess的声明,使其同时接受左值和右值。
重载是可行的(一个重载将声明一个左值引用形参,另一个重载将声明一个右值引用形参),但是我们需要维护两个函数。
避免这种情况的一种方法是让authAndAc cess使用一个可以绑定到左值和右值的引用形参,第24项 解释了这正是通用引用的作用。
因此,authAndAccess可以这样声明:

// c is now a universal reference
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c,Index i);

In this template, we don’t know what type of container we’re operating on, and that means we’re equally ignorant of the type of index objects it uses. Employing pass-byvalue for objects of an unknown type generally risks the performance hit of unnecessary copying, the behavioral problems of object slicing (see Item 41), and the sting of our coworkers’ derision, but in the case of container indices, following the example of the Standard Library for index values (e.g., in operator[] for std::string, std::vector, and std::deque) seems reasonable, so we’ll stick with pass-by-value for them.

在这个模板中,我们不知道我们正在操作什么类型的容器,这意味着我们同样不知道它使用的索引对象的类型(即:c[i]返回类型)。 对未知类型的对象使用 pass-byvalue 按值传递 通常会有不必要的复制对性能造成的影响、对象切片的行为问题(参见条款 41)以及我们同事的嘲笑,但在容器索引的情况下,遵循 用于索引值的标准库示例(例如,在operator[] 用于 std::string、std::vector 和 std::deque)似乎是合理的,因此我们将坚持为它们传递值(这段话大概意思就是,我们函数既然要返回容器下标成员,那返回值应该和 对标准库operator[] 操作所得到的返回值类型一致,没有必要单独考虑值传递还是引用传递)。

但是,我们需要更新模板的实现,使其符合第25项的警告:将std::forward(完美转发函数:保持原始参数的类型,将实参从原来的类型为右值引用的左值,变成了本身就是右值引用)应用于通用引用:

template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i)
{authenticateUser(); return std::forward<Container>(c)[i];
}

这应该完成我们想要的一切,但它需要一个c++ 14编译器。如果没有,就需要使用该模板的c++ 11版本。它与c++ 14版本相同,不同的是你必须自己指定返回类型:

template<typename Container, typename Index>
// final C++11 version
auto authAndAccess(Container&& c, Index i)
-> decltype(std::forward<Container>(c)[i])
{authenticateUser(); return std::forward<Container>(c)[i];
}

另一个可能会困扰您的问题是我在本项目开头的评论,即decltype几乎总是产生您所期望的类型,它很少让您感到惊讶。
说实话,除非您是一个重量级的库实现者,否则不太可能遇到这些规则的例外。

要完全理解decltype的行为,您必须熟悉一些特殊情况。
其中的大多数都太过晦涩,不足以在这样的书中进行讨论,但是研究其中的一个有助于深入了解decltype及其使用。

将decltype应用于名称将产生该名称声明的类型。名称是左值表达式,但这并不影响decltype的行为。但是,对于比名称更复杂的左值表达式,decltype确保报告的类型始终是左值引用。也就是说,如果除名称以外的左值表达式具有类型T, decltype将该类型报告为T&。这很少有任何影响,因为大多数左值表达式的类型内在地包含一个左值引用限定符。例如,返回左值的函数总是返回左值引用。

然而,这种行为有一个隐含的含义,值得注意。在

int x = 0;

其中 x 是变量的名称,因此 decltype(x) 是 int。 但是将名称 x 括在括号中——“(x)”——会产生一个比名称更复杂的表达式。 作为一个名称,x 是一个左值,C++ 也将表达式 (x) 定义为一个左值。 decltype((x)) 因此是 int&。 在名称周围加上括号可以更改 decltype 为其报告的类型!

在c++ 11中,这只是一种好奇,但结合c++ 14对decltype(auto)的支持,这意味着在编写return语句的方式上的一个看似微不足道的改变就会影响函数的推导类型:

decltype(auto) f1()
{int x = 0; …return x;     // decltype(x) is int, so f1 returns int
}
decltype(auto) f2()
{int x = 0; …return (x); // decltype((x)) is int&, so f2 returns int&
}

注意,f2不仅返回类型与f1不同,它还返回对局部变量的引用!
这种代码会把你送上通向未定义行为的快车——一辆你肯定不想乘坐的快车。

主要的经验是在使用decltype(auto)时要非常注意。
被推导类型的表达式中看似无关紧要的细节可能会影响decltype(auto)报告的类型。为了确保所推导的类型是您所期望的类型,请使用第4项中描述的技术。

同时,不要忽视大局(decltype的主要用法是稳定的)。
当然,decltype(单独使用或者与auto一起使用)可能偶尔会产生类型推导上的意外,但这不是正常的情况。通常,decltype会生成您期望的类型。

当decltype应用于名称时尤其如此,因为在这种情况下,decltype就像它听起来的那样:它报告名称声明的类型。

记住:

  • Decltype几乎总是在不做任何修改的情况下生成变量或表达式的类型。
  • 对于类型T而不是名称的左值表达式,decltype总是报告类型T&。
  • c++ 14支持decltype(auto),与auto一样,它从初始化器中推断类型,但它使用decltype规则执行类型推断。

第4项:知道如何查看推导出的类型。

查看类型推断结果的工具的选择取决于您想要获取信息的软件开发过程的阶段。
我们将探索三种可能性:在编辑代码时获取类型演绎信息,在编译期间获取信息,以及在运行时获取信息。

IDE编辑器

当你将光标悬停在实体上时,ide中的代码编辑器通常会显示程序实体的类型(例如,变量、参数、函数等)。
例如,给定这段代码,

const int theAnswer = 42;
auto x = theAnswer;
auto y = &theAnswer;

IDE编辑器可能会显示x的推断类型是int,而y的推断类型是const int*。

要使其工作,代码必须处于或多或少的可编译状态,因为使IDE能够提供这种信息的是在IDE中运行的c++编译器(或至少前端)。
如果编译器不能充分理解您的代码来解析它并执行类型推断,它就不能显示它推导出的类型。

对于像int这样的简单类型,来自IDE的信息通常很好。
但是,我们很快就会看到,当涉及到更复杂的类型时,IDE显示的信息可能不是特别有用。

编译器的诊断程序

让编译器显示它推导出的类型的一种有效方法是,以导致编译问题的方式使用该类型。报告问题的错误消息实际上肯定会提到导致问题的类型。(写错代码时vs就会提示红线,告诉你它理解的正确的类型应该是怎么样。)

例如,假设我们想看看在前面的例子中为x和y推导出的类型。
我们首先声明一个没有定义的类模板。
这样做就很好:

template<typename T> // declaration only for TD;
class TD;           // TD == "Type Displayer"

尝试实例化此模板将引发错误消息,因为没有要实例化的模板定义。 要查看 x 和 y 的类型,只需尝试使用它们的类型实例化 TD:

// 引出包含x和y类型的错误
TD<decltype(x)> xType;
TD<decltype(y)> yType;

对于上面的代码,我的一个编译器发出诊断读取,部分如下(我已经突出显示了我们所追求的类型信息):

error: aggregate 'TD<int> xType' has incomplete type and cannot be defined error: aggregate 'TD<const int *> yType' has incomplete type and cannot be defined

不同的编译器提供相同的信息,但形式不同:

error: 'xType' uses undefined class 'TD<int>' error: 'yType' uses undefined class 'TD<const int *>'

撇开格式差异不讲,当使用这种技术时,我测试过的所有编译器都会产生包含有用类型信息的错误消息。

运行时输出

显示类型信息的printf方法(我并不是建议您使用printf)直到运行时才能使用,但是它提供了对输出格式的完全控制。挑战在于创建适合显示的类型的文本表示。“没问题,”您可能会想,“它是typeid和std::type_info::name来拯救您。”
在我们继续探索x和y的类型推导过程中,您可能认为我们可以这样写:

std::cout << typeid(x).name() << '\n'; // display types for
std::cout << typeid(y).name() << '\n'; // x and y

这种方法依赖于这样一个事实:在x或y这样的对象上调用typeid会产生一个std::type_info对象,而std::type_info有一个成员函数name,该函数产生类型名的c风格字符串(即const char*)表示。

对std::type_info::name的调用不保证返回任何合理的结果,但是实现会尽量提供帮助。帮助的程度各不相同。
例如,GNU和Clang编译器报告x的类型是“i”,而y的类型是“PKi”。
一旦您了解到,在这些编译器的输出中,“i”表示“int”,而“PK”表示“pointer to konst const.”,这些结果就有意义了。
(两个编译器都支持一个工具,c++filt,可以解码这种“混乱的”类型。)
微软的编译器产生更少的隐式输出:“int”用于x和“int const *”用于y。

因为这些结果对于x和y的类型是正确的,您可能会认为类型报告问题已经解决,但是我们不要草率。考虑一个更复杂的例子:

template<typename T>
void f(const T& param); //template function to be calledstd::vector<Widget> createVec(); // factory functionconst auto vw = createVec();// init vw w/factory return
if (!vw.empty())
{ f(&vw[0]);…
}

这段代码包含一个用户定义的类型(Widget)、一个STL容器(std:: vector)和一个auto变量(vw),它更能代表您可能希望对编译器推导出的类型有一些可见性的情况。例如,最好知道模板类型参数T和f中的函数形参param推断出了什么类型。

放任typeid处理这个问题很简单。只需要添加一些代码到f来显示你想要看到的类型:

template<typename T>
void f(const T& param)
{using std::cout;cout << "T = " << typeid(T).name() << '\n'; // show Tcout << "param = " << typeid(param).name() << '\n';…
}

GNU和Clang编译器生成的可执行文件产生如下输出:

T = PK6Widget
param = PK6Widget

我们已经知道,对于这些编译器,PK意味着“指向const的指针”,所以唯一的谜是数字6。这只是后面的类名中的字符数(Widget)。
因此,这些编译器告诉我们,T和param都是const Widget*类型。

微软的编译器一致:

T = class Widget const *
param = class Widget const *

三个独立的编译器产生了相同的信息,表明该信息是准确的。但仔细看看。
在模板f中,param声明的类型是const T&。
在这种情况下,T和param有相同的类型是不是看起来很奇怪?
例如,如果T是int, param的类型应该是const int& -而不是完全相同的类型。

遗憾的是,std::type_info::name的结果不可靠。
例如,在这种情况下,所有三个编译器为param报告的类型都是不正确的。
此外,它们本质上必须是不正确的,因为std:: type_info::name的规范要求将该类型当作按值参数传递 by-value 给模板函数来处理。
正如第1项所解释的,这意味着如果类型是引用,它的引用性将被忽略,如果删除引用后的类型是const(或volatile),它的常量性(或volatile)也将被忽略。
这就是为什么param的正确类型(const Widget* const &)却被报告为const Widget*的原因。
首先去掉类型的引用,然后去掉结果指针的常量性。

同样令人遗憾的是,IDE编辑器显示的类型信息也不可靠——或者至少不可靠地有用。对于这个相同的例子,我知道的一个IDE编辑器报告T的类型为(我不是编的):

const
std::_Simple_types<std::_Wrap_alloc<std::_Vec_base_types<Widget, std::allocator<Widget> >::_Alloc>::value_type>::value_type *

相同的IDE编辑器显示param的类型如下:

const std::_Simple_types<...>::value_type *const &

这不像 T 的类型那么令人生畏,但是中间的“…”会让人感到困惑,直到你意识到这是 IDE 编辑器的表达方式:“我忽略了属于 T 类型的所有内容。” 运气好的话,你的开发环境在这样的代码上做得更好。

如果您更倾向于依赖库而不是运气,那么您会很高兴地知道,在std::type_info::name和ide可能失败的地方,Boost TypeIndex库(通常写成Boost.TypeIndex)被设计成成功。
该库不是标准c++的一部分,但也不是ide或模板(如TD)的一部分。
此外,Boost库(可以在boost.com上找到)是跨平台的、开源的,并且在一个许可下可以使用,即使是最偏执的公司法律团队也可以接受,这意味着使用Boost库的代码几乎和依赖于标准库的代码一样可移植。

以下是我们的函数 f 如何使用 Boost.TypeIndex 生成准确的类型信息:

#include <boost/type_index.hpp>
template<typename T>
void f(const T& param)
{using std::cout; using boost::typeindex::type_id_with_cvr;// show T cout << "T =   "<< type_id_with_cvr<T>().pretty_name() << '\n';// show param's type cout << "param = "<< type_id_with_cvr<decltype(param)>().pretty_name() << '\n';…
}

它的工作方式是函数模板 boost::typeindex::type_id_with_cvr 接受一个类型参数(我们想要信息的类型)并且不删除 const、volatile 或引用限定符(因此模板中的“with_cvr” 姓名)。 结果是一个 boost::typeindex::type_index 对象,它的 pretty_name 成员函数产生一个 std::string 包含该类型的可读的表示。

对于 f 的这个实现,再次考虑在使用 typeid 时为 param 产生不正确类型信息的调用:

std::vector<Widget> createVec(); // factory function
const auto vw = createVec();
if (!vw.empty())
{ f(&vw[0]);…
}

在 GNU 和 Clang 的编译器下,Boost.TypeIndex 产生这个(准确的)输出:

T = Widget const*
param = Widget const* const&

微软编译器下的结果基本相同:

T = class Widget const *
param = class Widget const * const &

这种近乎一致性很好,但重要的是要记住 IDE 编辑器、编译器错误消息和 Boost.TypeIndex 之类的库只是您可以用来帮助您确定编译器正在推导哪些类型的工具。 一切都会有所帮助,但归根结底,没有什么可以替代理解第1项到第3项中的类型推断信息。

记住:

  • 推导出的类型通常可以通过IDE编辑器、编译器错误消息和Boost TypeIndex库看到。

  • 一些工具的结果可能既没有帮助也不准确,所以理解c++的类型推导规则仍然是必要的。


第二章:auto

​ 在概念上,auto 尽可能简单,但它比看起来更微妙。 使用它可以节省打字,当然,但它也可以防止可能困扰手动类型声明的正确性和性能问题。 此外,auto 的一些类型推导结果,虽然尽职尽责地符合规定的算法,但从程序员的角度来看,这是错误的。 在这种情况下,重要的是要知道如何引导 auto 找到正确的答案,因为回到手动类型声明是一种通常最好避免的替代方案。
这一简短的章节涵盖了auto的所有细节。

第5项:选择auto而不是explicit显式类型声明。

啊,简单的快乐

int x;

等等,该死的。我忘了初始化x,所以它的值是不确定的。也许吧。它实际上可能被初始化为0。这取决于上下文。叹息。

不要紧。让我们继续简单的乐趣:通过对迭代器解引用来声明一个局部变量来初始化:

// algorithm to dwim ("do what I mean")  for all elements in range from b to e
template<typename It>
void dwim(It b, It e)
{while (b != e) {typename std::iterator_traits<It>::value_type                   currValue = *b;… }
}

啊。" typename std::iterator_traits::value_type "表示迭代器指向的值的类型?真的吗?我一定是忘记了那有多好玩。该死的。等等,我不是已经说过了吗?

好的,简单的快乐(第三个):声明一个类型为闭包的局部变量的快乐。 啊对。 闭包的类型只有编译器知道,因此不能写出来。 叹。 该死。

该死,该死,该死!用c++编程并不是它应有的快乐体验!

好吧,以前不是。但是在c++ 11中,所有这些问题都消失了,感谢auto。
auto变量的类型是从它们的初始化式推导出来的,因此必须对它们进行初始化。
这意味着,当你在现代c++高速公路上快速行驶时,你可以向大量未初始化的变量问题挥手告别(不初始化编不过):

int x1;  // potentially uninitialized
auto x2; // error! initializer required
auto x3 = 0; // fine, x's value is well-defined

上面所说的高速公路不像声明一个局部变量(它的值是解引用迭代器的值)那样复杂的表达式:

// algorithm to dwim ("do what I mean")  for all elements in range from b to e
template<typename It>
void dwim(It b, It e)
{while (b != e) {auto currValue = *b;  // 改成这样,简单… }
}

由于auto使用类型推导(见第2项),它可以表示只有编译器知道的类型:

//比较函数,指向Widget类型的std::unique_ptrs指针(智能指针的一种)
auto derefUPLess = [](const std::unique_ptr<Widget>& p1, const std::unique_ptr<Widget>& p2)
{ return *p1 < *p2; };

非常酷。在c++ 14中,情绪激动程度进一步下降,因为lambda表达式的参数可能涉及auto:

//c++ 14比较函数,用于比较任何类指针所指向的值
auto derefLess = [](const auto& p1, const auto& p2){ return *p1 < *p2; };

虽然很酷,但也许您认为我们实际上并不需要auto来声明一个保存闭包的变量,因为我们可以使用std::function对象。没错,我们可以,但可能你不是这么想的。
也许现在您在想“什么是std::function对象?”我们把它弄清楚。

function是c++ 11标准库中的一个模板,它推广了函数指针的概念。
然而,函数指针只能指向函数,而std::function对象可以指向任何可调用对象,也就是说,指向任何可以像函数一样调用的对象。
正如在创建函数指针时必须指定要指向的函数类型(即,要指向的函数的签名)一样,在创建std::function对象时也必须指定要引用的函数类型。
可以通过std::function 的模板形参来实现这一点。
例如,要声明一个名为func的std::function对象,该对象可以引用任何可调用对象,就好像它具有这样的签名一样,

// C++11 signature for std::unique_ptr<Widget>comparison function; std::unique_ptr<Widget>比较函数的c++ 11签名
bool(const std::unique_ptr<Widget>&, const std::unique_ptr<Widget>&)

你会写这样:

std::function<bool(const std::unique_ptr<Widget>&, const std::unique_ptr<Widget>&)> func;

因为lambda表达式产生可调用对象,闭包可以存储在std::function对象中。
这意味着我们可以声明c++ 11版本的derefUP Less而不使用auto,如下所示:

std::function<bool(const std::unique_ptr<Widget>&, const std::unique_ptr<Widget>&)>
derefUPLess = [](const std::unique_ptr<Widget>& p1, const std::unique_ptr<Widget>& p2) { return *p1 < *p2; };

重要的是要认识到,即使不考虑语法冗长和需要重复参数类型,使用std::function与使用auto也是不同的。
持有闭包的auto声明变量具有与闭包相同的类型,因此它只使用闭包所需的内存。
std::function声明的变量包含一个闭包,它的类型是std::function模板的一个实例化,对于任何给定的签名,它都有一个固定的大小。
这个大小可能不足以存储它被要求存储的闭包,在这种情况下,std::function构造函数将分配堆内存来存储闭包。
结果是,std::function对象通常比自动声明的对象使用更多的内存。
而且,由于限制内联和产生间接函数调用的实现细节,通过std::function对象调用闭包几乎肯定比通过自动声明的对象调用慢。
换句话说,std:: function方法通常比auto方法更大、更慢,而且还可能产生内存不足的异常。
另外,正如您在上面的示例中看到的那样,编写“auto”比编写 std::function 实例化的类型要少得多。
在auto和std::function之间的竞争中,它几乎是游戏,设置和匹配的就是auto。(类似的参数也可以用auto代替std::function来保存std::bind调用的结果,但是在第34项中,我尽量说服您使用lambdas而不是std::bind。)

auto扩展的优点不仅仅是避免未初始化的变量、冗长的变量声明以及直接持有闭包的能力。一个是能够避免我所说的与“类型捷径”相关的问题。
以下是一些你可能见过甚至写过的东西:

std::vector<int> v;
…
unsigned sz = v.size();  // unsigned == unsigned int

v.size()的标准返回类型是std::vector::size_type,但很少有开发人员知道这一点。Std::vector::size_type被指定为无符号整型,因此许多程序员认为无符号就足够好了,于是编写了上面这样的代码。这可能会产生一些有趣的后果。例如,在32位Windows上,unsigned和std::vector::size_type大小相同,但在64位Windows上,unsigned是32位,而std::vector::size_type是64位。
这意味着在32位Windows下运行的代码可能在64位Windows下运行不正确,当将应用程序从32位移植到64位时,谁愿意花时间在这样的问题上?

使用 auto 就可确保您不必在上边说的问题上花时间了:

auto sz = v.size();
// sz的类型是 std::vector<int>::size_type

仍然不确定使用auto是否明智?然后考虑下面的代码:

std::unordered_map<std::string, int> m;
…for (const std::pair<std::string, int>& p : m)  // 遍历m
{… // do something with p
}

这看起来很合理,但有一个问题。你看到了吗?

想知道出什么问题,你需要知道 std::unor dered_map 哈希表的key部分是 const,因此哈希表中 std::pair 的类型(即 std::unordered_map 的类型)不是 std::pair< std::string, int>,它是 std::pair <const std::string, int>。 但这不是上面循环中为变量 p 声明的类型。 因此,编译器将努力寻找一种将 std::pair<const std::string, int> 对象(即哈希表中的内容)转换为 std::pair<std::string, int> 对象的方法 (p 的声明类型)。 他们将通过复制 m 中的每个对象来创建 p 想要绑定的类型的临时对象,然后将引用 p 绑定到该临时对象来成功。 在每次循环迭代结束时,临时对象将被销毁。 如果你写了这个循环,你可能会对这种行为感到惊讶,因为你只是打算简单地将引用 p 绑定到 m 中的每个元素。

这种无意的类型不匹配可以自动消除:

for (const auto& p : m)
{… // as before
}

这不仅更有效率,也更容易打字。此外,这段代码还有一个非常吸引人的特性,即如果你取p的地址,你肯定会得到一个指向m内元素的指针。在不使用auto的代码中,你会得到一个指向临时对象的指针——这个对象会在循环迭代结束时被销毁。

最后两个例子——当你应该写 std::vec tor::size_type 时写成了 unsigned 和当你应该写 std::pair<const std::string, int >时写成了 std::pair<std::string, int>,演示了显式explicit指定类型如何导致您既不想要也不期望的隐式转换。 如果使用 auto 作为目标变量的类型,则不必担心要声明的变量类型与用于初始化它的表达式类型之间的不匹配。

因此,有几个理由更喜欢 auto 而不是显式explicit类型声明。 然而,auto并不完美。 每个auto变量 的类型是从其初始化表达式推导出来的,有些初始化表达式的类型既不是预期的,也不是期望的。 出现这种情况的条件,以及如何处理这些情况,在第2项和第6项中进行了讨论(auto声明一个变量,然后用带大括号的初始化式初始化它,推导出来的类型将始终是std::initializer_list),因此我不会在这里讨论它们。 相反,我将把注意力转向您可能对使用 auto 代替传统类型声明的另一个担忧:生成的源代码的可读性。

首先,深呼吸,放松。auto是一种选择,而不是强制。
如果根据您的专业判断,通过使用显式explicit类型声明,您的代码会更清晰、更易于维护,或者在其他方面会更好,那么您可以继续使用它们。
但请记住,C++ 在采纳 编程语言世界中通常称为“类型推断”的东西方面,并没有开辟新天地。
其他静态类型的过程性语言(如c#、D、Scala、Visual Basic)或多或少都有类似的特性,更不用说各种静态类型的函数性语言(如ML、Haskell、OCaml、f#等)。
在某种程度上,这是由于动态类型语言的成功,如Perl、Python和Ruby,这些语言很少显式地输入变量。
软件开发社区在类型推断方面有丰富的经验,并且已经证明了这种技术与大型的、工业级代码库的创建和维护之间没有矛盾。

使用auto消除了通过快速查看源代码来确定对象类型的能力,这让一些开发人员感到不安。
但是,IDE显示对象类型的能力通常可以缓解这个问题(即使考虑到第4项中提到的IDE类型显示问题),而且,在许多情况下,对象类型的稍微抽象的视图与确切的类型一样有用。
例如,通常只知道一个对象是容器、计数器或智能指针就足够了,而不知道它到底是哪种容器、计数器或智能指针。
假设变量名选择得很好(用变量名弥补auto类型的隐式声明),这样的抽象类型信息几乎总是随手可得。

事实是,显式地编写类型通常只会带来细微错误的机会,无论是在正确性还是效率方面,都可能出现问题。
此外,如果auto类型的初始化表达式的类型发生变化,auto类型也会自动改变,这意味着使用auto可以简化某些重构。
例如,如果函数声明返回int类型,但您后来认为long类型更好,如果调用函数的结果存储在自动变量中,则调用代码将在下次编译时自动更新自身。
如果结果存储在显式声明为int的变量中,则需要找到所有调用点,以便修改它们。

记住:

  • auto变量必须进行初始化,通常不会出现导致可移植性或效率问题的类型不匹配,可以简化重构过程,通常比显式指定类型的变量需要更少的打字键入。
  • auto-typed的变量容易出现第2项和第6项中描述的缺陷。

第6项:当auto推断出不希望得到的类型时,请使用显式类型化初始化式惯用语。

第5项解释了使用auto来声明变量比显式指定类型提供了许多技术优势,但有时当您希望使用auto时,它的类型推断会出现曲折。
例如,假设我有一个函数,它接受一个Widget并返回std::vector,其中每个bool表示Widget是否提供特定的特性:

std::vector<bool> features(const Widget& w);

进一步假设features返回值的第5位表示Widget是否具有高优先级。因此,我们可以这样编写代码:

Widget w;
…
bool highPriority = features(w)[5];// is w high priority?
…
// process w in accord with its priority
processWidget(w, highPriority);

这段代码没有任何错误。它会正常工作。但是,如果我们将highPriority的显式类型替换为auto,

auto highPriority = features(w)[5]; // is w high priority?

情况发生了变化。所有代码都将继续编译,但其行为不再可预测:

processWidget(w, highPriority); // undefined behavior!

正如注释所指出的,对processWidget的调用现在具有未定义的行为。
但是为什么呢?答案可能会令人惊讶。
在使用auto的代码中,highPriority的类型不再是bool。
尽管std::vector在概念上保存了bool类型,但std::vector的操作符[]不返回对容器元素的引用(这是std::vector::operator[]对除bool类型以外的所有类型返回的结果)。
相反,它返回std::vector::reference类型的对象(嵌套在std::vector中的类)。

// vs2019 演示代码
class Widget{
private:int a;
};std::vector<bool> features(const Widget& w) {std::vector<bool> vb1 = {true, false,true, false, true, false, true};return vb1;
}
void processWidget(Widget& w, bool b) { ; }int main(void)
{Widget w;//vs2019返回类型 std::_Vb_reference<std::_Wrap_alloc<std::allocator<unsigned int>>>auto highPriority = features(w)[5];
// can not dereference value-initiazed vector<bool> itreatorprocessWidget(w, highPriority);return 1;
}

std::vector::reference 存在是因为 std::vector 被指定以打包的方式表示其bools值,每个bool值一位。 这给 std::vector 的 operator[] 带来了问题,因为 std::vector 的 operator[] 应该返回 T&,但 C++ 禁止引用bits。 无法返回 bool&,std::vector 的 operator[] 返回一个行为类似于 bool& 的对象。 为了使这一行为成功,std::vector::reference 对象必须在 bool&s 可以使用的所有上下文中都可用。 在 std::vec tor::reference 中实现这项工作的特性之一是隐式转换为 bool。 (不是 bool&,而是 bool。解释 std::vec tor::reference 用来模拟 bool& 行为的全套技术会让我们走得太远,因此我将简单地指出,这种隐式转换只是更大的组合中的一块石头。)

有了这些信息,再看一下原始代码的这一部分:

bool highPriority = features(w)[5];
// 显式声明highPriority的类型

在这里,features返回一个std::vector对象,在该对象上调用操作符[]。
operator[]返回一个std::vector::reference 对象,然后隐式转换为初始化highPriority所需的bool类型。
因此,highPriority以特性返回的std::vector中第5位的值结束,就像它应该的那样。

对比一下自动声明的高优先级:

auto highPriority = features(w)[5];
// deduce highPriority's type

同样, features 返回一个 std::vector 对象,并且再次在其上调用 operator[] 。 operator[] 继续返回 std::vector::reference 对象,但现在发生了变化,因为 auto 将其推断为 highPriority 的类型。 highPriority 根本没有 返回的 std::vector 的第 5 位的值 这样的特性。

它的值取决于如何实现 std::vector::reference。一种实现是让此类对象包含一个指针,该指针指向保存被引用位的机器字,以及该位在该字中的偏移量。考虑一下这对highPriority的初始化意味着什么,假设这样的std::vector::reference实现已经到位。

对features的调用返回一个临时的std::vector对象。这个对象没有名称,但出于本讨论的目的,我将它称为temp;
operator[]是在temp上调用的,它返回的std::vector::reference 包含一个指向数据结构中包含由temp管理的bits的一个WORD 的指针,加上该WORD对应于第5位的偏移量。
highPriority是std::vector::reference 对象的副本,因此highPriority也包含一个指向temp中的一个WORD的指针,加上对应于第5位的偏移量。
在语句的末尾,销毁temp,因为它是一个临时对象。
因此,highPriority包含一个悬空指针,这就是调用processwidget时出现未定义行为的原因:

processWidget(w, highPriority);
// undefined behavior! highPriority contains dangling pointer!

Std::vector::reference是代理类的一个示例:这个类的存在是为了模拟和扩展某些其他类型的行为。代理类被用于各种各样的目的。
例如,std::vector::reference的存在提供了一种错觉,即操作符[]对std::vector返回对一个bit的引用,而标准库的智能指针(smart pointer)类型(参见第四章)是将资源管理移植到原始指针上的代理类。
代理类的实用程序是很完善的。
事实上,设计模式“Proxy”是软件设计模式先贤祠中最悠久的成员之一。

有些代理类被设计为对客户端可见。
例如,std::shared_ptr和std::unique_ptr就是这样。
其他代理类被设计成或多或少不可见的行为。
Std::vector::reference就是这种“不可见”代理的一个例子,它的Std::bitset同胞Std::bitset::ref也是如此。

在这个阵营中还有一些c++库中的类,它们使用了一种称为表达式模板的技术。
开发这些库最初是为了提高数字代码的效率。
例如,

Matrix sum = m1 + m2 + m3 + m4;

给定一个矩阵类和矩阵对象m1、m2、m3和m4,如果矩阵对象的operator+返回结果的代理而不是结果本身,那么表达式的计算效率就会高得多。
也就是说,两个Matrix对象的operator+将返回一个代理类的对象,如Sum<Matrix, Matrix>,而不是一个Matrix对象。
与std::vector::reference和bool的情况一样,将有一个从代理类到Matrix的隐式转换,这将是从" = "右边的表达式产生的代理对象 来初始化sum。
(该对象的类型通常会对整个初始化表达式进行编码,例如Sum<Sum<Sum<Matrix, Matrix>, Matrix>, Matrix>。这绝对是客户端应该屏蔽的一种类型。)

作为一般规则,“不可见”代理类不能很好地与auto配合使用。这些类的对象通常不能设计为比单个语句的生存时间更长,因此创建这些类型的变量往往会违反基本的库设计假设。std::vector::reference 就是这种情况,我们已经看到,违反该假设可能会导致未定义的行为。

因此,您需要避免以下形式的代码:

auto someVar = expression of "invisible" proxy class type;

但是,如何识别代理对象何时在使用中?使用它们的软件不太可能宣传它们的存在。它们应该是看不见的,至少在概念上是这样!一旦你找到了它们,你真的必须放弃auto和第5项 所展示的许多优势吗?

让我们先来看看如何找到他们的问题。 尽管“不可见”代理类被设计为在日常使用中避开程序员的雷达,但使用它们的库经常记录他们这样做。 您对使用的库的基本设计决策熟悉得越多,您就越不可能被这些库中的代理使用弄得措手不及。

在资料文档不足的地方,头文件填补了空白(帮你找到代理类的存在)。 源代码几乎不可能完全隐藏代理对象。 它们通常是从期望客户端调用的函数返回的,因此函数签名通常反映它们的存在。 这是 std::vector::operator[] 的规范,例如:

namespace std {  // from C++ Standardstemplate <class Allocator> class vector<bool, Allocator> {public: …class reference { … };reference operator[](size_type n); …};
}

假设您知道std::vector的operator[]通常返回一个T&,那么在本例中operator[]的非常规返回类型(operator[]没有返回Allocator &)是正在使用代理类的提示。
仔细注意正在使用的接口通常可以揭示代理类的存在。

在实践中,许多开发人员只有在试图追踪令人费解的编译问题或调试不正确的单元测试结果时才发现使用代理类。 无论您如何找到它们,一旦确定 auto 是在推断代理类的类型而不是被代理的类型,解决方案就不需要放弃 auto。 auto本身不是问题。 问题是auto不能推断出你想让它推断的类型。 解决方案是强制进行不同类型的推断。 这就是我所说的显式类型初始化式习惯用法。

显式类型的初始化式习惯用法包括使用auto声明变量,但将初始化表达式转换为希望auto推导的类型。
下面是如何使用它强制highPriority为bool类型,例如:

auto highPriority = static_cast<bool>(features(w)[5]);

在这里,features(w)[5]继续返回std::vector::reference对象,就像它总是那样,但强制转换将表达式的类型改为bool,然后自动推导为highPriority的类型。
在运行时,从std::vector::operator[]返回的std::vectorreference 对象将执行到它支持的bool类型的转换,作为转换的一部分,从特性返回的指向std::vector的仍然有效的指针将被解引用。这就避免了我们之前遇到的未定义行为。然后将索引5应用于指针所指向的位,并使用出现的bool值初始化highPriority。

对于矩阵的例子,显式类型的初始化式习惯用法是这样的:

auto sum = static_cast<Matrix>(m1 + m2 + m3 + m4);

这种习惯用法的应用不仅限于生成代理类类型的初始化器。
强调您有意创建的变量类型与初始化表达式生成的变量类型不同,这一点也很有用。例如,假设你有一个函数来计算公差值:

double calcEpsilon();    // 返回公差值

calcEpsilon显然返回一个double,但是假设您知道对于您的应用程序,浮点数的精度就足够了,并且您关心浮点数和双精度数之间的大小差异。
你可以声明一个浮点变量来存储calcEpsilon的结果,

// 隐式转换double→float
float ep = calcEpsilon();

但这几乎不会宣布“我故意降低了函数返回的值的精度”。然而,使用显式类型初始化式惯用语的声明可以:

auto ep = static_cast<float>(calcEpsilon()); // 宣布使用float

如果有意将浮点表达式存储为整型值,也可以采用类似的推理。
假设你需要用随机访问迭代器(例如std::vector, std::deque,或者std::array)计算容器中元素的索引,你得到了一个介于0.0到1.0之间的双精度值,表示所需元素距离容器的起始位置有多远。(0.5表示容器的中间。)进一步假设您确信生成的索引适合于一个整数。如果容器是c,双精度浮点数是d,你可以用这种方式计算下标,

int index = d * c.size();

但这掩盖了一个事实,即你故意将右边的双精度浮点数转换为int。
显式类型的初始化式习惯用法使事情变得透明:

auto index = static_cast<int>(d * c.size());

书外补充信息:

class B {};
class D : public B {};
void f(B* pb, D* pd) {// 不安全,D可以有B中没有的字段和方法(pb可能就是指向一个基类对象)D* pd2 = static_cast<D*>(pb);   // 安全转换,D总是包含所有的B。B* pb2 = static_cast<B*>(pd);
}//static_cast不进行运行时检查,无法保证绝对安全;

记住:

  • “不可见”代理类型可能导致 auto 推断出初始化表达式的“错误”类型。
  • 显式类型化初始化式习惯用法强制自动推断您希望它具有的类型。

第三章:转向现代c++ (Moving to Modern C++)

说到著名的特性,c++ 11和c++ 14有很多值得夸耀的地方。auto、智能指针、move语义、lambdas、并发性——每一个都是如此重要,我用了一章来介绍。
掌握这些特性是必要的,但是成为一个高效的现代c++程序员还需要一系列更小的步骤。每一步都回答了从c++ 98到现代c++过程中出现的特定问题。
什么时候应该使用花括号{}而不是圆括号()来创建对象?为什么别名声明比typedefs更好?constexpr和const有什么不同?const成员函数和线程安全之间的关系是什么?
这样的例子数不胜数。本章逐一给出了答案。

第7项:创建对象时区分圆括号(parentheses)和花括号{braces}

根据您的观点,c++ 11中对象初始化的语法选择要么丰富得令人尴尬,要么混乱得令人困惑。一般规则是,初始化值可以用圆括号、等号或花括号指定:

int x(0);    // 初始化式在()中
int y = 0;  // 初始化式 在“=” 后边
int z{ 0 }; //初始化式 在{}中

在很多情况下,也可以同时使用等号和花括号{}:

int z = { 0 }; // 使用 "=" 和{} 初始化

对于本项目的其余部分,我通常会忽略等号加花括号语法,因为C++通常将其视为仅用花括号版本相同。

“令人困惑的混乱”指出 The “confusing mess” lobby points out,在初始化中使用等号经常会误导c++新手,让他们认为正在进行赋值,尽管实际上并没有。对于像int这样的内置类型,区别是学术性的,但对于用户定义的类型,区分初始化和赋值很重要,因为涉及到不同的函数调用:

class Widget;
Widget w1;  // call default constructor
Widget w2 = w1;    // 不是赋值,调用拷贝构造
w1 = w2;   // 赋值; calls copy operator=

即使使用了几种初始化语法,在某些情况下,c++ 98仍然无法表达所需的初始化。
例如,c++ 98不支持直接创建一个包含一组特定值(例如 1、3 和 5)的 STL 容器.

为了解决多种初始化语法的混乱,以及它们没有涵盖所有初始化场景的事实,c++ 11引入了统一初始化(uniform initialization):一个单一的初始化语法,至少在概念上,可以在任何地方使用,并表达一切。它基于花括号,因此我更喜欢使用带花括号的初始化术语。“统一初始化”是一种思想。“花括号的初始化”是一种语法结构。

带括号的初始化允许您表达以前无法表达的内容。使用花括号来指定容器的初始内容很容易:

std::vector<int> v{ 1, 3, 5 };
// v's initial content is 1, 3, 5

花括号{}还可用于指定非静态数据成员的默认初始化值。此功能(C++11 的新功能)与“=”初始化语法共享,但不与括号()共享:

class Widget { …
private: int x{ 0 };    // fine, x's default value is 0int y = 0;         // also fineint z(0);       // error!!!
};
// 疑惑: {} = 初始化,与构造函数初始化式有什么差异?性能?

另一方面,不可复制对象(如std::atomics -参见第40项)可以使用花括号{}或圆括号()初始化,但不能使用" = ":

std::atomic<int> ai1{ 0 }; // fine
std::atomic<int> ai2(0);  // fine std::atomic<int> ai3 = 0;    // error!

因此,很容易理解为什么带括号的初始化被称为“统一”。在c++指定初始化表达式的三种方法中,只有花括号可以到处使用。

带花括号{}的初始化的一个新特性是,它禁止内置类型之间的隐式收缩转换(narrowing conversions)。如果带花括号{}的初始化式中表达式的值不能被被初始化对象的类型所表达,代码将无法编译:

double x, y, z;
…
// error!双精度浮点数的和不能表示为int
int sum1{ x + y + z };

使用圆括号和" = "进行初始化不会检查缩小转换(narrowing conversions),因为这会破坏太多的遗留代码:

// (表达式的值被截断为整数)
int sum2(x + y + z);int sum3 = x + y + z; // ditto

带花括号{}的初始化的另一个 值得注意的特点是它不受c++ 最恼人的解析(most vexing parse)的影响。
c++规则的一个副作用是,任何可以被解析为声明的东西都必须被解释为声明,当开发人员想要默认构造一个对象时,最恼人的解析(most vexing parse)最经常困扰他们,但却无意中声明了一个函数。问题的根源在于,如果你想调用带参数的构造函数,你可以这样做,

Widget w1(10);
// call Widget ctor with argument 10

但是如果你试图使用类似的语法调用一个零参数的Widget类构造函数,你声明的是一个函数而不是一个对象:

Widget w2();
// most vexing parse! 声明了一个名为w2 返回值是Widget的函数!

函数不能使用花括号声明形参列表,所以默认使用花括号构造对象不会有这个问题:

Widget w3{}; // calls Widget ctor with no args

因此,对于带花括号{}的初始化有很多可说的。它是一种可以在最广泛的上下文中使用的语法,它可以防止隐式的收缩转换(narrowing conversions),并且不受c++最恼人的解析(most vexing parse)的影响。三连冠的好!那么为什么这个项目的标题不是类似于“偏好带花括号的初始化语法”呢?

带花括号的初始化 的缺点是有时会伴随令人惊讶的行为。这种行为产生于带花括号的初始化器、std::initializer_lists和构造函数重载解析 之间异常复杂的关系。
它们之间的交互可能导致代码看起来应该做一件事,但实际上却做了另一件事。
例如,第2项解释说,当一个auto的变量有一个带花括号的初始化式时,推导出的类型是std::initializer_list,尽管使用相同的初始化式声明变量的其他方式会产生更直观的类型。因此,您越喜欢auto,就越不可能对 带花括号的初始化 感兴趣。

在构造函数调用中,只要不涉及std::initializer_list参数,圆括号和花括号的含义相同:

class Widget
{ // 构造函数没有声明std::initializer_list参数
public:Widget(int i, bool b); Widget(int i, double d);…
};Widget w1(10, true);  // calls first ctor构造
Widget w2{10, true};    // also calls first ctor
Widget w3(10, 5.0);     // calls second ctor
Widget w4{10, 5.0};     // also calls second ctor

但是,如果一个或多个构造函数声明了一个std::initializer_list类型的参数,那么使用带花括号的初始化语法的调用将强烈偏好重载std::initializer_lists。强烈。
如果编译器有任何方法可以将使用花括号初始化程序的调用解释为使用 std::initial izer_list 的构造函数,编译器将采用该解释。
例如,如果上面的Widget类增加接受std::initializer_list参数的构造函数,

class Widget {
public:Widget(int i, bool b); Widget(int i, double d);Widget(std::initializer_list<long double> il); // added...
}

Widget类对象w2和w4将使用新的构造函数来构造,尽管与非std::initializer_list构造函数相比,std::initializer_list元素的类型(长双精度)对于这两个参数的匹配程度更差!看:

// uses parens and, as before,  calls first ctor构造
Widget w1(10, true); // uses braces, but now calls std::initializer_list ctor (10 and true convert to long double)
Widget w2{10, true}; // uses parens and, as before, calls second ctor构造
Widget w3(10, 5.0); // uses braces, but now calls std::initializer_list ctor (10 and 5.0 convert to long double)
Widget w4{10, 5.0};

即使是通常的copy和move构造也会被std::initializer_list构造函数所劫持:

class Widget {
public:Widget(int i, bool b); Widget(int i, double d);Widget(std::initializer_list<long double> il); // addedoperator float() const; // convert to float...
}// uses parens, calls copy ctor 拷贝构造
Widget w5(w4); // uses braces, calls std::initializer_list ctor (w4 converts to float, and float converts to long double)
Widget w6{w4};// uses parens, calls move ctor 移动构造
Widget w7(std::move(w4)); // uses braces, calls std::initializer_list ctor (for same reason as w6)
Widget w8{std::move(w4)};

编译器将带花括号的初始化式与接受std::initializer_lists的构造函数匹配的决心是如此强烈,即使最匹配的std::initializer_list构造函数不能被调用,它也会占上风。
例如:

class Widget {
public:Widget(int i, bool b); Widget(int i, double d);// element type is now boolWidget(std::initializer_list<bool> il);...// 没有隐式转换函数
}Widget w{10, 5.0}; // 错误!需要缩小转换narrowing conversions

在这里,编译器将忽略前两个构造函数(第二个构造函数提供了两个参数类型的精确匹配),并尝试调用接受std::initializer_list参数的构造函数。
调用该构造函数需要将一个int(10)和一个double(5.0)转换为bool类型。
这两种转换都将是收缩转换(bool不能准确地表示任何一个值),而收缩转换(narrowing conversions)在带花括号的初始化式中是禁止的,因此调用无效,代码将被拒绝。

只有当没有办法将带花括号的初始化式中的参数类型转换为std::initializer_list中的类型时,编译器才会采用正常的重载解析。例如,如果我们将std::initializer_list构造函数替换为接受std::initializer_list<std:: string>的构造函数,非std::initializer_list构造函数再次成为候选构造函数,因为没有办法将int和bool类型转换为std::string类型:

class Widget {
public:Widget(int i, bool b); Widget(int i, double d);// // std::initializer_list元素类型现在是std::stringWidget(std::initializer_list<std::string> il);… // 没有隐式转换函数
};Widget w1(10, true); // uses parens, still calls first ctor
Widget w2{10, true}; // uses braces, now calls first ctor
Widget w3(10, 5.0); // uses parens, still calls second ctor
Widget w4{10, 5.0}; // uses braces, now calls second ctor

至此,我们对带花括号的初始化式和构造函数重载的研究接近尾声,但还有一个有趣的边界情况需要解决。假设您使用一个空的花括号集合来构造一个对象,该对象支持默认构造,同时也支持std::initializer_list构造。你的空括号是什么意思?
如果它们的意思是“没有参数”,你会得到默认构造,但是如果它们的意思是“空std::initializer_list”,你会从没有元素的std::initializer_list得到构造。

规则是获得默认结构。空括号表示没有参数,而不是空的std::initializer_list:

class Widget {
public:Widget();// std::initializer_list ctorWidget(std::initializer_list<int> il);… // 没有隐式转换函数
};Widget w1; // calls default ctor
Widget w2{}; // also calls default ctor
Widget w3();// most vexing parse! declares a function!

如果你想用一个空的std::initializer_list调用一个std::initializer_list构造函数,你可以把空花括号作为构造函数的参数——把空花括号放在圆括号或大括号中来划分你要传递的内容:

// calls std::initializer_list ctor with empty list
Widget w4({});Widget w5{{}};// ditto

此时,随着关于带花括号的初始化式、std::initial izer_lists和构造函数重载的看似神秘的规则在您的脑海中反复出现,您可能想知道这些信息在日常编程中有多重要。可能比您想象的要多,因为直接受影响的类之一是std::vector。
Std::vector有一个非Std::initializer_list构造函数,它允许您指定容器的初始大小和每个初始元素应有的值,但它还有一个接受Std::initializer_list的构造函数,它允许您指定容器中的初始值。如果你创建了一个数值类型的std::vector(例如,std::vector),并将两个参数传递给构造函数,将这些参数括在圆括号中还是花括号中有很大的不同:

// use non-std::initializer_list  ctor: create 10-element std::vector, all elements have value of 20
std::vector<int> v1(10, 20);// use std::initializer_list ctor: create 2-element std::vector, element values are 10 and 20
std::vector<int> v2{10, 20};

但是,让我们从std::vector以及圆括号、花括号和构造函数重载解析规则的细节上后退一步。从这个讨论中可以得出两个主要结论。首先,作为类的作者,您需要注意,如果您的重载构造函数集包括一个或多个接受std::ini tializer_list的函数,那么使用带花括号的初始化的客户端代码可能只会看到调用std::ini tializer_list重载。因此,最好设计构造函数,使所调用的重载不受客户端是否使用圆括号或花括号的影响。换句话说,从std:: vector接口设计中现在被视为错误的地方吸取教训,并在设计类时避免它。

这意味着如果您有一个没有 std::initializer_list 构造函数的类,并且添加了一个支持std::initializer_list 的构造函数fun2,则使用花括号初始化的客户端代码可能会发现用于解析为非 std::initializer_list 构造函数的调用现在解析为新构造函数fun2。 当然,这种事情可能发生在你向一组重载中添加一个新函数的任何时候:用于解析一个旧重载的调用可能会开始调用新的重载。 与 std::initializer_list 构造函数重载的区别在于,std::initializer_list 重载不仅与其他重载竞争,而且使它们黯然失色,以至于其他重载几乎不被考虑。 因此,只有经过深思熟虑才能添加此类重载。

The second lesson is that as a class client, you must choose carefully between parentheses and braces when creating objects. Most developers end up choosing one kind of delimiter as a default, using the other only when they have to. Braces-by-default folks are attracted by their unrivaled breadth of applicability, their prohibition of narrowing conversions, and their immunity to C++’s most vexing parse. Such folks understand that in some cases (e.g., creation of a std::vector with a given size and initial element value), parentheses are required. On the other hand, the goparentheses-go crowd embraces parentheses as their default argument delimiter. They’re attracted to its consistency with the C++98 syntactic tradition, its avoidance of the auto-deduced-a-std::initializer_list problem, and the knowledge that their object creation calls won’t be inadvertently waylaid by std::initial izer_list constructors. They concede that sometimes only braces will do (e.g., when creating a container with particular values). There’s no consensus that either approach is better than the other, so my advice is to pick one and apply it consistently.

第二个教训是,作为类客户端,创建对象时必须在圆括号和花括号之间谨慎选择。大多数开发人员最终选择一种分隔符作为默认分隔符,仅在必要时使用另一种分隔符。使用默认花括号的人被其无与伦比的适用性广度、对收缩转换(narrowing conversions)的禁止以及对 C++ 最令人烦恼的解析(most vexing parse)的免疫力所吸引。这些人明白在某些情况下(例如,创建具有给定大小和初始元素值的 std::vector)需要圆括号。另一方面,“goparenthes-go”的拥趸将圆括号作为默认的参数分隔符。它们被它与c++ 98语法传统的一致性所吸引,它避免了auto-deduced-a-std::initializer_list问题,并且知道它们的对象创建调用不会被std::initializer_list构造函数无意中拦截。他们承认有时只有花括号才行(例如,在创建具有特定值的容器时)。两种方法孰优孰劣还没有达成共识,所以我的建议是选择一种并始终如一地应用它。

If you’re a template author, the tension between parentheses and braces for object creation can be especially frustrating, because, in general, it’s not possible to know which should be used. For example, suppose you’d like to create an object of an arbitrary type from an arbitrary number of arguments. A variadic template makes this conceptually straightforward:

如果您是模板作者,创建对象时圆括号和花括号之间的紧张关系可能会特别令人沮丧,因为通常情况下,不可能知道应该使用哪个。例如,假设您想用任意数量的参数创建任意类型的对象。可变参数模板使这在概念上很简单:

// type of object to create, types of arguments to use
template<typename T,typename... Ts>
void doSomeWork(Ts&&... params)
{create local T object from params... …
}

有两种方法可以将这行伪代码转换为实际代码(关于std::forward的信息参见第25项):

T localObject(std::forward<Ts>(params)...);// 圆括号
T localObject{std::forward<Ts>(params)...};// 花括号

考虑下面的调用代码:

std::vector<int> v;
…
doSomeWork<std::vector<int>>(10, 20);

如果doSomeWork在创建localObject时使用了圆括号,结果将是一个带有10个元素的std::vector。如果doSomeWork使用大括号,结果是一个有两个元素10, 20 的std:: vector。哪个是正确的?doSomeWork的作者不可能知道。只有调用者知道。

这正是标准库函数std::make_unique和std::make_shared所面临的问题(见第21项)。这些函数通过在内部使用圆括号并将此决定记录为其接口的一部分来解决这个问题(More flexible designs—ones that permit callers to determine whether parentheses or braces should be used in functions generated from a template—are possible. For details, see the 5 June 2013 entry of Andrzej’s C++ blog, “Intuitive interface — Part I | Andrzej’s C++ blog (wordpress.com).”)。

记住:

  • 带花括号的初始化是最广泛使用的初始化语法,它可以防止缩小转换(narrowing conversions)范围,并且不受c++最烦人的解析(most vexing parse)的影响。
  • 在构造函数重载决议期间,如果可能的话,花括号初始值设定项与 std::initializer_list 参数匹配,即使其他构造函数提供看似更好的匹配。
  • 圆括号和花括号之间的选择可以产生显着差异的一个示例是创建一个带有两个参数的 std::vector。
  • 在圆括号和花括号之间选择在模板内创建对象可能具有挑战性。

第8项:优先使用nullptr而不是0和NULL。

所以事情是这样的:0字面值是一个int,而不是一个指针。
如果c++发现自己在只能使用指针的上下文中查看0,它会勉强地将0解释为空指针,但这是一种退路。c++的主要策略是0是整型,而不是指针。

实际上,NULL也是如此。NULL的细节中有一些不确定性,因为实现允许给NULL一个除int以外的整型(例如,long)。这并不常见,但这并不重要,因为这里的问题不是NULL的确切类型,而是0和NULL都没有指针类型。

在c++ 98中,其主要含义是指针和整型的重载可能会导致意外。向这样的重载传递0或NULL从不调用指针重载:

void f(int); // three overloads of f
void f(bool);
void f(void*);f(0);         // calls f(int), not f(void*)// 可能无法编译,但通常调用 f(int)。 从不调用 f(void*)
f(NULL);

关于f(NULL)行为的不确定性反映了对于NULL类型的实现的灵活性。
如果NULL被定义为,比如说,0L(0是long类型),那么调用是二义性的,因为从long到int,从long到bool,从0L到void*的转换被认为是同样好的。关于这个调用,有趣的事情是源代码的表面含义(“我用空指针调用f”)和它的实际含义(“我用某种整数调用f -而不是空指针”)之间的矛盾。这种违反直觉的行为是c++ 98程序员避免指针和整型重载的指导原则。这条准则在c++ 11中仍然有效,因为尽管有这条条款的建议,有些开发人员可能会继续使用0和NULL,即使nullptr是更好的选择。

Nullptr的优点是它没有整数类型。老实说,它也没有指针类型,但您可以将其视为所有类型的指针。Nullptr的实际类型是std::nullptr_t,并且,在一个奇妙的循环定义中,std::nullptr_t被定义为Nullptr的类型。类型std::nullptr_t隐式转换为所有原始指针类型,这使得nullptr的行为就像它是所有类型的指针一样。

用nullptr调用重载函数f会调用f(void*)重载(即指针重载),因为nullptr不能被视为任何整数:

f(nullptr);  // calls f(void*) overload

使用nullptr而不是0或NULL可以避免重载解析的意外,但这不是它唯一的优点。
它还可以提高代码的清晰度,特别是当涉及到auto变量时。例如,假设你在一个代码库中遇到这个:

auto result = findRecord( /* arguments */ );
if (result == 0)
{...
}

如果您不知道(或者不容易找到)findRecord返回类型,那么可能就不清楚结果是指针类型还是整型。毕竟,0(测试结果)可能会有两种结果。另一方面,如果你看到下面这些,

auto result = findRecord( /* arguments */ );
if (result == nullptr)
{ …
}

没有歧义:result必须是指针类型。

当模板映入眼帘时,Nullptr特别醒目。假设有一些函数只有在锁定了适当的互斥锁时才应该调用。每个函数接受不同类型的指针:

// 只有在适当的互斥锁被锁定时才调用它们
int f1(std::shared_ptr<Widget> spw);
double f2(std::unique_ptr<Widget> upw);
bool f3(Widget* pw);

调用想要传递空指针的代码可以像这样:

std::mutex f1m, f2m, f3m; // mutexes for f1, f2, and f3// C++11 typedef; see Item 9
using MuxGuard = std::lock_guard<std::mutex>;
…
{// 将0作为null ptr传递给f1MuxGuard g(f1m); // lock mutex for f1auto result = f1(0);
}
…
{// 将NULL作为null ptr传递给f2解锁互斥锁MuxGuard g(f2m); // lock mutex for f2auto result = f2(NULL);
}
…
{//将nullptr作为null ptr传递给f3MuxGuard g(f3m); // lock mutex for f3 auto result = f3(nullptr);
}

在此代码的前两个调用中未能使用 nullptr 是可悲的,但代码有效,这很重要。 然而,调用代码中的重复代码:——锁定互斥体、调用函数、解锁互斥体——不仅令人难过。 这令人不安。 这种源代码重复是模板旨在避免的事情之一,所以让我们将模式模板化:

template<typename FuncType, typename MuxType, typename PtrType>
auto lockAndCall( FuncType func, MuxType& mutex,PtrType ptr) -> decltype(func(ptr))
{MuxGuard g(mutex); return func(ptr);
}

如果这个函数的返回类型(auto…-> decltype(func(ptr))让你摸不着头脑,帮你个忙,导航到第3项,它解释了发生了什么。你会看到在c++ 14中,返回类型可以简化为简单的decltype(auto):

template<typename FuncType, typename MuxType, typename PtrType>
decltype(auto) lockAndCall( FuncType func, MuxType& mutex, PtrType ptr)
{MuxGuard g(mutex); return func(ptr);
}

给定lockAndCall模板(任一版本),调用者可以这样编写代码:

auto result1 = lockAndCall(f1, f1m, 0);// error!
…
auto result2 = lockAndCall(f2, f2m, NULL);// error!
…
auto result3 = lockAndCall(f3, f3m, nullptr); // fine

好吧,他们可以编写它,但是,正如注释所示,在这三种情况中的两种,代码无法编译。第一个调用的问题是,当0被传递给lockAndCall() 时,模板类型推导就开始了,以确定它的类型。0的类型是int,过去是,将来总是int,这就是lockAndCall实例化过程中参数ptr的类型。不幸的是,这意味着在lockAndCall内部对func的调用中,传递了一个int,这与f1期望的std::shared_ptr参数不兼容。在对lockAndCall的调用中传递的0表示一个空指针,但实际上传递的是一个普通的int。
试图将这个int作为std::shared_ptr 传递给f1是一个类型错误。使用 0 调用 lockAndCall 失败,因为在模板内部,一个 int 被传递给一个需要 std::shared_ptr 的函数。

对于涉及NULL的调用的分析本质上是相同的。当NULL传递给lockAndCall时,参数ptr推导出int,当ptr传递给f2时发生类型错误,ptr是int类型或 类int类型,它期望得到的却是std::unique_ptr。

相比之下,涉及nullptr的调用就没有问题。当nullptr被传递给lockAndCall时,ptr的类型被推导为std::nullptr_t。当ptr被传递给f3时,有一个从std::nullptr_t到Widget*的隐式转换,因为std::nullptr_t支持隐式转换为所有指针类型。

模板类型推导 推导出 0 和 NULL 的“错误”类型(即它们的真实类型,而不是它们作为空指针表示的后备含义)的事实,是使用 nullptr 而不是 0 或 NULL 的最令人信服的理由 你想引用一个空指针。对于nullptr,模板不会带来特别的挑战。加上nullptr不会遇到0和NULL容易遇到的重载解析问题,这种情况是可靠的。当您想要引用空指针时,使用nullptr,而不是0或null。

记住:

  • 优先使用nullptr而不是0和NULL
  • 避免对 整型和指针类型 重载

第9项:优先使用alias declarations而不是typedefs

我相信我们都同意 使用STL容器是一个好主意,我希望第18项说服您使用std::unique_ptr是一个好主意,但我猜我们都不喜欢不止一次地编写“std::unique_ptr<std::unor dered_map<std::string, std::string>>”类型。
光是想想就可能增加患腕管综合症的风险。

避免这样的医疗悲剧很容易。 引入一个 typedef:

typedef std::unique_ptr<std::unordered_map<std::string,                            std::string>> UPtrMapSS;

但是 typedefs 太 C++98 了。 当然它们在 C++11 中也能工作,但 C++11 也提供了别名声明(alias declarations):

using UPtrMapSS = std::unique_ptr<std::unordered_map<std::string, std::string>>;

既然typedef和别名声明(alias declarations)所做的事情完全相同,那么我们有理由怀疑是否有可靠的技术理由来选择其中一个而不是另一个。

有,但在开始之前,我想提一下,许多人发现在处理涉及函数指针的类型时,别名声明(alias declarations)更容易理解:

// FP是指向函数的指针的同义词,该函数接受一个int类型和一个const std::string&类型,返回void
typedef void (*FP)(int, const std::string&);// typedef//  采用 alias declaration
using FP = void (*)(int, const std::string&);

当然,这两种形式都不是特别容易理解的,而且很少有人会花很多时间处理函数指针类型的同义词,所以这并不是一个令人信服的理由来选择别名声明(alias declarations)而不是typedefs。

但一个令人信服的原因确实存在:模板。特别地,别名声明可以被模式化(在这种情况下,它们被称为别名模板 alias templates ),而typedefs不能。这为c++ 11程序员提供了一种直接的机制来表达在c++ 98中必须与模板化结构内嵌套的类型定义一起处理的内容。例如,考虑为使用自定义分配器MyAlloc的链表定义一个同义词synonym 。使用别名模板,这是小菜一碟:

// MyAllocList<T>是std::list<T, MyAlloc<T>>的同义词
template<typename T>
using MyAllocList = std::list<T, MyAlloc<T>>;MyAllocList<Widget> lw;// client code

使用typedef,你几乎必须从头开始创建蛋糕:

//MyAllocList<T>::type是std::list<T, MyAlloc<T>>的同义词
template<typename T>
struct MyAllocList {typedef std::list<T, MyAlloc<T>> type;
}MyAllocList<Widget>::type lw; // client code

它变得更糟。如果你想在模板中使用typedef来创建一个包含模板形参指定类型的对象的链表,你必须在typedef名称前加上typename:

// Widget<T> 包含MyAllocList<T>作为数据成员
template<typename T>
class Widget {
private:typename MyAllocList<T>::type list;...
}
// 实测不加 typename, MyAllocList<T>:: 不被认为是类型
template<typename T>
struct MyAllocList {typedef std::vector<T> type;
};template<typename T>
class Widget2 {
private:// warning C4346: "type": 依赖名称不是类型//  error C2061: 语法错误: 标识符“type”MyAllocList<T>::type vector2;
};int main(void){return 1;
}

这里,MyAllocList::type指的是一个依赖于模板类型参数(T)的类型。因此,MyAllocList::type是一个依赖类型(dependent type),C++ 的许多可爱规则之一是依赖类型(dependent type)的名称必须以 typename 开头。

如果MyAllocList被定义为别名模板,那么typename就不再需要了(就像繁琐的"::type "后缀一样):

template<typename T>
using MyAllocList = std::list<T, MyAlloc<T>>; // as beforetemplate<typename T>
class Widget {
private:MyAllocList<T> list; // no "typename", no "::type"…
};

对您来说,MyAllocList(即使用别名模板)可能看起来与MyAllocList::type(即使用嵌套的typedef)一样依赖于模板参数T,但您不是编译器。当编译器处理Widget模板并遇到MyAllocList的使用(即 使用别名模板alias template)时,它们知道MyAllocList是一个类型的名称,因为MyAllocList是一个别名模板:它必须命名一个类型。因此,MyAllocList是一个非依赖类型,typename说明符既不需要也不允许。

另一方面,当编译器在Widget模板中看到MyAllocList::type(即 使用嵌套的typedef)时,编译器不能确定它命名了一个类型,因为可能有一个专门化的MyAllocList,编译器还没有看到MyAllocList::type引用类型以外的其他东西。这听起来很疯狂,但是不要将这种可能性归咎于编译器。正是人类创造了这样的代码。

例如,一些误入歧途的灵魂可能会编造出这样的东西:

class Wine { … };template<>    // //当T是Wine时MyAllocList特殊化
class MyAllocList<Wine> {
private:// see Item 10 for info on "enum class"enum class WineType { White, Red, Rose };// in this class, type is a data member!WineType type;…
};

可以看到,MyAllocList::type没有引用类型。如果用 Wine 实例化上边的 Widget模板类,那么Widget模板中的MyAllocList::type将引用一个数据成员,而不是类型。那么,在 Widget 模板中,MyAllocList::type 是否是 引用了一个类型实际上取决于 T 是什么,这就是为什么编译器坚持通过在它前面加上typename来断言它是一个类型。

如果您曾经做过任何模板元编程(TMP),那么您几乎肯定遇到过获取模板类型参数并根据它们创建修改后的类型的需要。例如,给定某种类型T,您可能想要去掉T包含的任何const或引用限定符,例如,您可能想要将const std::string&转换为std::string。或者你可能想要向一个类型添加const或将其转换为左值引用,例如,将Widget转换为const Widget或Widget&。(如果您没有做过任何TMP,那就太糟糕了,因为如果您想成为真正有效的c++程序员,您至少需要熟悉c++这方面的基础知识。您会在第23项和第27项中看到TMP的例子,包括我刚才提到的类型转换。)

c++ 11为您提供了以类型特征(type traits)的形式执行这些类型转换的工具,这是头文件<type_traits>中的模板组合。该头文件中有几十个类型特征(type traits),并不是所有的都执行类型转换,但它们确实提供了可预测的接口。给定要应用转换的类型T,结果类型是std::transformation ::type。例如:

std::remove_const<T>::type // 从const T得到T
std::remove_reference<T>::type // yields T from T& and T&&
std::add_lvalue_reference<T>::type // yields T& from T

注释仅仅总结了这些转换所做的工作,所以不要从字面上理解它们。
我知道,在项目中使用它们之前,你会查阅精确的说明书。

无论如何,我在这里的动机并不是要给你一个关于类型特征(type traits)的教程。
相反,注意这些转换的应用需要在每次使用结束时写入"::type "。如果您将它们应用于模板中的类型参数(实际上在实际代码中总是这样使用它们),您还必须在每次使用之前加上typename。出现这两种语法衰减的原因是,c++ 11的类型特征(type traits)是作为模板化结构中的嵌套typedefs 实现的。没错,它们是使用类型同义词(type synonym)技术实现的,我一直试图让您相信它不如别名模板!

这是有历史原因的,但我们将跳过它(我保证它很无聊),因为标准化委员也慢慢地认识到别名模板( alias templates)是更好的方法,他们在c++ 14中为所有的c++ 11类型转换(type transformations)包含了别名模板( alias templates)。他们的命名有一种通用形式:对于每个c++ 11转换td::transformation::type,都有一个对应的c++ 14别名模板std::transformation_t。下面的例子可以说明我的意思:

std::remove_const<T>::type // C++11: const T → T
std::remove_const_t<T>    // C++14 equivalentstd::remove_reference<T>::type // C++11: T&/T&& → T
std::remove_reference_t<T>    // C++14 equivalentstd::add_lvalue_reference<T>::type // C++11: T → T&
std::add_lvalue_reference_t<T>    // C++14 equivalent

c++ 11概念在c++ 14中仍然有效,但我不知道为什么要使用C++14新接口。即使您没有使用c++ 14的权限,自己编写别名模板也是轻而易举的事情。只需要c++ 11语言特性,甚至小孩子也可以模仿模式,对吧?如果您碰巧有c++ 14 Standard的电子版本,那就更容易了,因为所需要的只是一些复制和粘贴。来,我让你开始:

template <class T>
using remove_const_t = typename remove_const<T>::type; template <class T>
using remove_reference_t = typename remove_reference<T>::type; template <class T>
using add_lvalue_reference_t = typename add_lvalue_reference<T>::type;

看?再简单不过了。

记住:

  • typedefs 不支持模板化,但别名声明(using)支持。
  • 别名模板避免使用“::type”后缀,在模板中,通常需要使用“typename”前缀来指定 typedefs类型。
  • C++14 为所有C++11 类型特征转换提供别名模板

第10项:优先使用 有作用域的枚举而不是无作用域的枚举

Effective Modern C++ 纯人工翻译,持续更新,不为博你眼球,旨在自我提升相关推荐

  1. 【Spring Boot官方文档原文理解翻译-持续更新中】

    [Spring Boot官方文档原文理解翻译-持续更新中] 文章目录 [Spring Boot官方文档原文理解翻译-持续更新中] Chapter 4. Getting Started 4.1. Int ...

  2. 《Effective C++》学习笔记(持续更新)

    此文由来 <Effective C++>同<C++ primier>一样,也是非常出名的一本书,正如此书的副标题所说--改善程序与设计的55个具体做法,此书的目的,就是教会读者 ...

  3. 《Effective Java》 读书笔记(持续更新)

    2.1 用静态工厂方法代替构造器 静态工厂方法: 不通过 new (如:Date date = new Date();) 而是用一个静态方法来对外提供自身实例的方法叫做静态工厂方法(Static fa ...

  4. 初学Verilog语言基础笔记整理(实例点灯代码分析)持续更新~

    实例:点灯学习 一.Verilog语法学习 1. 参考文章 刚接触Verilog,作为一个硬件小白,只能尝试着去理解,文章未完-持续更新. 参考博客文章: Verilog语言入门学习(1) Veril ...

  5. 架构师学习笔记(持续更新)

    1.此博客所有内容均出自于咕泡学院架构师第三期课程. 2.此博客整理了我所学习的课程的所有笔记链接. 3.此博客会持续更新新的博客链接,直到课程学习完. 4.此博客仅供参考,仅作为学习使用. 设计模式 ...

  6. 《财富自由之路》——博多.舍费尔(持续更新中)2020-11-05

    <财富自由之路>--博多.舍费尔(持续更新中) 重新定义金钱的概念 自我分析财务状况 创造奇迹 你真正想要的是什么 责任意味着什么 重新定义金钱的概念 人应该热爱财富,同样有义务创造财富. ...

  7. android studio安装教程(持续更新中,包安装成功,不成功你找我)

    遇到问题请往下看,先看完,先看完,先看完!!! 如果实在解决不了可以联系我,评论区有联系方式!!! 百度搜索Android studio,或者直接输入Download Android Studio & ...

  8. Redis-6.2.x版本官方发行说明(附谷歌翻译)【持续更新】

    一.前言   本文只是单纯地翻译Redis官方的6.2.x版本的发行说明,不会对发行说明内容做任何改动,读者如果觉得有异议,可自行去Redis官方相关网页查阅.翻译工具翻译出来的不一定百分百准确,英语 ...

  9. 【持续更新】SDN Software Defined Networks(Thomas D.Nadeau Ken Gray)翻译

    [持续更新]SDN Software Defined Networks(Thomas D.Nadeau & Ken Gray)翻译 接下来的一段日子里,希望大家监督我把这本书读完. 自己翻译, ...

最新文章

  1. RAC -代替OC 中的代理
  2. NBT:噬菌体激发根际防御军团(附视频)
  3. 百篇大计敬本年之C++坎坷之路 —— Warning:will be initialized after [-Wreorder]
  4. OCI读取单条记录(C)
  5. 解决 Callout位置不更新的问题
  6. javascript的输入与输出
  7. GPS NMEA-0183协议详解
  8. cobbler 配置(转载)
  9. 第五天总结 运算符 职业化 运算符优先级 职业精神
  10. 第三方服务-极光推送
  11. 哈萨比斯首次解读AlphaZero竟被当场diss,他起身当面回击说…
  12. 项目方说性能达到百万TPS,如何测试它的可信度?
  13. YII with()
  14. MyBatis的XML配置文件(二)
  15. pandas.DataFrame对行和列求和及添加新行和列
  16. 计算机制图应用领域,计算机制图对测绘工程的应用
  17. 云主机搭建Git服务器 1
  18. 公用IPv6 IPv4 DNS
  19. 嵌入式Linux(5):驱动开发网络调试驱动设备的Linux系统移植
  20. 农用旋涡泵行业调研报告 - 市场现状分析与发展前景预测

热门文章

  1. Linux 下 ps 命令
  2. 我们想要招一个人,帮我们做公众号(兼职)
  3. 动手学深度学习之softmax实现
  4. 查看windows10是否永久激活
  5. 时间服务器(time server)名单
  6. 分析生产和库存,靠这一套指标就够了!
  7. C语言getchar()函数理解及其用法
  8. C语言 强行给内存地址赋值
  9. Springboot整合PageOffice 实现word在线编辑保存。
  10. 常用的函数图像绘制工具(网站)