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

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-const

int x = 27;  		// as before
const int cx = x; 	// as before
const int& rx = x;	// as before

f(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 int

f(&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 before

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&&

// 个人验证结果

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 pointerstype
	cout << 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 value

int 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 pointerstype
	cout << 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 int
	f(rx);  // T's and param's types are still both int
	return 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<ty
  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值