- Effective.Modern.C++
- 关键词翻译
- Argument
- Parameter
- ParamType
- expr 、expression
- type deduction
- Introduction 入门
- 术语和约定
- 报告bug并提出改进建议
- 第一章:推理类型
- 第1项:理解模板类型推导
- 情况1:ParamType 是引用或指针,但不是通用引用
- 情况3:ParamType 既不是指针也不是引用
- 数组参数
- 函数参数
- 第2项:了解自动类型推导
- 第3项:理解decltype
翻译原则: 一些我认为有争议的词句、我理解不了的,我会尽量保留英文原文,等确认清楚后再删除英文原文!!!
关键词翻译 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类型推导
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’s most pervasive feature is probably move semantics, and the foundation of move semantics is distinguishing expressions that are rvalues from those that are lvalues. That’s because rvalues indicate objects eligible for move operations, while lvalues generally don’t. In concept (though not always in practice), rvalues correspond to temporary objects returned from functions, while lvalues correspond to objects you can refer to, either by name or by following a pointer or lvalue reference.
c++ 11最普遍的特性可能是 移动语义(move semantic),而 移动语义(move semantic)的基础是区分右值和左值表达式。
这是因为右值表示适合移动操作的对象,而左值通常不适合。
在概念上(虽然在实践中并不总是),右值对应从函数返回的临时对象,而左值对应的是可以通过名称或跟随指针或左值引用(int &)引用的对象。
A useful heuristic to determine whether an expression is an lvalue is to ask if you can take its address. If you can, it typically is. If you can’t, it’s usually an rvalue. A nice feature of this heuristic is that it helps you remember that the type of an expression is independent of whether the expression is an lvalue or an rvalue. That is, given a type T, you can have lvalues of type T as well as rvalues of type T. It’s especially important to remember this when dealing with a parameter of rvalue reference type, because the parameter itself is an lvalue:
确定表达式是否为左值的有用启发式方法是 询问是否可以获取其地址。
如果可以的话,通常是左值。
如果你不能,它通常是右值。
这种启发式的一个很好的特性是,它帮助您记住表达式的类型与表达式是左值还是右值无关。
也就是说,给定一个T类型,你可以有T类型的左值,也可以有T类型的右值。在处理右值引用类型的形参时,特别重要的是要记住这一点,因为形参本身就是一个左值:
class Widget
{
public:
Widget(Widget&& rhs); // rhs is an lvalue, though it has
// an rvalue reference type
… };
在这里,在Widget的 移动构造函数 中获取rhs的地址是完全有效的,因此rhs是一个左值,尽管它的类型是一个右值引用。
(同理,所有形参 都是左值。)
这段代码演示了我通常遵循的几个约定:
-
类名是Widget。每当需要引用任意用户定义的类型时,我都会使用Widget。除非我需要显示类的特定细节,否则我使用Widget而不声明它。
-
我使用参数名称rhs(“右手边”)。它是移动操作(即,移动构造函数和移动赋值操作符)和复制操作(即,复制构造函数和复制赋值操作符)的首选参数名。我也用它来求二元算符的右参数:
Matrix operator+(const Matrix& lhs, const Matrix& rhs);
我希望这并不奇怪,lhs代表“左手边”.
-
我对部分代码或部分注释应用特殊格式,以引起您的注意。
在上面的Widget 移动构造函数中,我突出显示了rhs的声明以及注释中指出rhs是左值的部分。
高亮显示的代码既不是天生的好,也不是天生的坏。
这只是您应该特别注意的代码。 -
我用“…”来表示“其他代码可以放到这里”。
这个窄省略号不同于c++ 11可变参数模板源代码中使用的宽省略号(“…”)。
这听起来令人困惑,但事实并非如此。
例如:
emplate// these are C++ void processVals(const Ts&... params) // source code { // ellipses … // this means "some code goes here" }
processVals的声明表明,我在模板中声明类型形参 时使用了typename,但这只是个人偏好;关键字 class 也同样有效。
当我展示c++ Standard的代码摘录时,我使用class声明类型形参,因为这是标准的工作。
When an object is initialized with another object of the same type, the new object is said to be a copy of the initializing object, even if the copy was created via the move constructor. Regrettably, there’s no terminology in C++ that distinguishes between an object that’s a copy-constructed copy and one that’s a move-constructed copy:
当一个对象用另一个相同类型的对象初始化时,新对象被称为初始化对象的副本,即使这个副本是通过移动构造函数创建的。
遗憾的是,c++中没有术语区分复制构造的复制对象和移动构造的复制对象:
void someFunc(Widget w);// someFunc's parameter w is passed by value Widget wid; // wid is some Widget someFunc(wid); // in this call to someFunc, w is a copy of wid that's created via copy construction 拷贝构造 someFunc(std::move(wid)); // in this call to SomeFunc, w is a copy of wid that's created via move construction 移动构造
Copies of rvalues are generally move constructed, while copies of lvalues are usually copy constructed. An implication is that if you know only that an object is a copy of another object, it’s not possible to say how expensive it was to construct the copy. In the code above, for example, there’s no way to say how expensive it is to create the parameter w without knowing whether rvalues or lvalues are passed to someFunc. (You’d also have to know the cost of moving and copying Widgets.)
右值的副本通常是移动构造的,而左值的副本通常是复制构造的。
这意味着,如果您只知道一个对象是另一个对象的副本,那么就不可能说出构造这个副本的代价有多大。
例如,在上面的代码中,如果不知道传递给someFunc的是右值还是左值,就无法说明创建参数w的开销有多大。
(您还必须知道移动和复制Widgets的成本。)
In a function call, the expressions passed at the call site are the function’s arguments. The arguments are used to initialize the function’s parameters. In the first call to someFunc above, the argument is wid. In the second call, the argument is std::move(wid). In both calls, the parameter is w. The distinction between arguments and parameters is important, because parameters are lvalues, but the arguments with which they are initialized may be rvalues or lvalues. This is especially relevant during the process of perfect forwarding, whereby an argument passed to a function is passed to a second function such that the original argument’s rvalueness or lvalueness is preserved. (Perfect forwarding is discussed in detail in Item 30.)
在函数调用中,在调用点传递的表达式是函数的实参。
实参用于初始化函数的形参。
在上面对someFunc的第一个调用中,实参是 wid。
在第二个调用中,实参是std::move(wid)。
在这两次调用中,形参都是w。实参和形参之间的区别很重要,因为形参是左值,但用于初始化形参的实参可以是右值或左值。
这在 完美转发(perfect forward)过程中尤其相关,在 完美转发(perfect forward)过程中,传递给一个函数的实参被传递给第二个函数,这样原始实参的右值或左值被保留。
(完美转发详见第30条)
Well-designed functions are exception safe, meaning they offer at least the basic exception safety guarantee (i.e., the basic guarantee). Such functions assure callers that even if an exception is thrown, program invariants remain intact (i.e., no data structures are corrupted) and no resources are leaked. Functions offering the strong exception safety guarantee (i.e., the strong guarantee) assure callers that if an exception arises, the state of the program remains as it was prior to the call.
设计良好的函数是异常安全的,这意味着它们至少提供基本的异常安全保证(即基本保证)。
这样的函数保证调用者即使抛出异常,程序不变量仍然保持完整(也就是说,没有数据结构被破坏),没有资源被泄露。
提供强异常安全保证(即强保证)的函数向调用者保证,如果出现异常,程序的状态保持在调用之前的状态。
When I refer to a function object, I usually mean an object of a type supporting an operator() member function. In other words, an object that acts like a function. Occasionally I use the term in a slightly more general sense to mean anything that can be invoked using the syntax of a non-member function call (i.e., “function Name(arguments)”). This broader definition covers not just objects supporting oper ator(), but also functions and C-like function pointers. (The narrower definition comes from C++98, the broader one from C++11.) Generalizing further by adding member function pointers yields what are known as callable objects. You can generally ignore the fine distinctions and simply think of function objects and callable objects as things in C++ that can be invoked using some kind of function-calling syntax.
当我引用函数对象时,我通常指的是支持operator()成员函数的类型的对象。
换句话说,一个像函数一样的对象。
我偶尔会在更一般的意义上使用这个术语,表示可以使用非成员函数调用的语法调用的任何东西(即“函数名(参数)”)。
这个更广泛的定义不仅包括支持operator()的对象,还包括函数和类c函数指针。
(狭义的定义来自于c++ 98,广义的定义来自于c++ 11。)
通过添加成员函数指针进一步泛化,将产生所谓的可调用对象。
通常可以忽略这些细微的区别,简单地将函数对象和可调用对象看作是c++中可以使用某种函数调用语法调用的对象。
通过lambda表达式创建的函数对象被称为闭包。
很少有必要区分lambda表达式和它们创建的闭包,所以我经常将两者都称为lambdas。
类似地,我很少区分函数模板(即生成函数的模板)和模板函数(即从函数模板生成的函数)。
对于类模板和模板类也是如此。
c++中的许多东西可以同时声明和定义。
声明引入了名称和类型,但没有给出细节,比如存储在哪里或者东西是如何实现的:
extern int x; // object declaration 对象声明
class Widget; // class declaration 类声明
bool func(const Widget& w); // function declaration 函数声明
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
定义也可以作为声明,所以除非定义非常重要,否则我倾向于引用声明。
I define a function’s signature to be the part of its declaration that specifies parameter and return types. Function and parameter names are not part of the signature. In the example above, func’s signature is bool(const Widget&). Elements of a function’s declaration other than its parameter and return types (e.g., noexcept or constexpr, if present), are excluded. (noexcept and constexpr are described in Items 14 and 15.) The official definition of “signature” is slightly different from mine, but for this book, my definition is more useful. (The official definition sometimes omits return types.)
我将函数签名定义为其声明的一部分,用于指定参数和返回类型。
函数名和参数名不是签名的一部分。
在上面的例子中,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++的机会,我不能解决我不知道的问题!
FIX: 类型推导 (auto : type deduction )
c++ 98只有一组类型推导规则:函数模板规则。
c++ 11稍微修改了一下该规则集,并增加了两个规则集,一个用于auto,一个用于decltype。
然后,c++ 14扩展了auto和decltype可以使用的上下文。
类型推导的日益广泛的应用,使您 可以从拼写出“明显的或冗余的类型” 的强制规则中解脱出来。
它使c++软件的适应性更强,因为在源代码中的某一点更改类型会自动通过类型推导传播到其他位置。
但是,它会使呈现的代码变得更难理解,因为编译器推导出的类型可能不像您希望的那样明显。
如果没有对类型推导如何运作的深刻理解,现代 C++ 中的有效编程几乎是不可能的。 发生类型推导的上下文太多了:在调用函数模板时,在 auto 出现的大多数情况下,在 decltype 表达式中,以及从 C++14 开始,使用神秘的 decltype(auto) 构造。
本章提供了每个c++开发人员都需要的关于类型推导的信息。
它解释了模板类型推导是如何工作的,“自动类型” 如何在此基础上构建,以及decltype如何按照自己的方式进行。
它甚至解释了如何强制编译器使其 类型推导 的结果可见,从而使您能够确保编译器推导出您希望它们推导的类型。
当一个复杂系统的用户不知道它是如何工作的,但对它所做的事情感到满意时,这就说明了系统设计的重要性。通过这种方法,C++中的模板类型推导取得了巨大的成功。数百万程序员已将参数传递给模板函数,并取得了完全令人满意的结果,尽管其中许多程序员很难给出关于这些函数使用的类型是如何推导的最模糊的描述。
如果你也在其中,我有一个好消息和一个坏消息。好消息是,模板的类型推导是现代c++最引人注目的特性之一的基础:自动(auto)。如果您对c++ 98为模板推导类型感到满意,那么您也会对c++ 11为auto推导类型感到满意。坏消息是,当模板类型推导规则应用于auto上下文中时,它们有时看起来不如应用于模板时直观。因此,真正理解自动构建所依赖的模板类型推导的各个方面是很重要的。这个项目涵盖了你需要知道的。
如果您愿意忽略一些伪代码,我们可以将函数模板想象为如下所示:
templatevoid f(ParamType param); // !!!注意,后文经常提到ParamType
调用可以是这样的:
f(expr); // call f with some expression
在编译过程中,编译器使用expr推导出两种类型:一种用于T,另一种用于ParamType。这些类型通常是不同的,因为ParamType通常包含修饰符,例如const或引用限定符。例如,如果模板像这样声明,
templatevoid f(const T& param);// ParamType is const T&
像这样调用时,
int x = 0; f(x);// call f with an int
T被推导为int,但ParamType被推导为const int&。
It’s natural to expect that the type deduced for T is the same as the type of the argument passed to the function, i.e., that T is the type of expr. In the above example, that’s the case: x is an int, and T is deduced to be int. But it doesn’t always work that way. The type deduced for T is dependent not just on the type of expr, but also on the form of ParamType. There are three cases:
我们很自然地认为,为T推导出的类型与传递给函数的参数类型相同,即T是expr的类型。
在上面的例子中,情况就是这样:x是一个int,那么T被推导为int。但情况并不总是这样。
为T推导出的类型不仅依赖于expr 这个 调用参数表达式 的类型,还依赖于ParamType的类型构成。
有三种情况:
- ParamType 是指针或引用类型,但不是通用引用(即: &&因为它可以解释为很多种引用)。
(通用引用见第24项。此时,您只需要知道它们的存在,并且它们与左值引用或右值引用不同。) - ParamType 是一个通用引用(即: &&)。
- ParamType 既不是指针也不是引用。
因此,我们有三种类型的推导场景要检查。每一个都将基于模板的一般形式和对它的调用:
template情况1:ParamType 是引用或指针,但不是通用引用void f(ParamType param); f(expr); // 从expr推导T和ParamType
最简单的情况是,ParamType是引用类型或指针类型,但不是通用引用。
在这种情况下,类型推导是这样的:
- 如果expr 的类型是引用,忽略引用部分。
- 然后将expr的类型与ParamType进行模式匹配,以确定T。
例如,如果这是我们的模板,
templatevoid 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的引用在类型推导过程中被忽略。
这些示例都显示了左值引用形参,但类型推导与右值引用形参的工作方式完全相同。当然,只有右值参数 可以传递给右值引用形参,但这一限制与类型推导无关。
If we change the type of f’s parameter from T& to const T&, things change a little, but not in any really surprising ways. The constness of cx and rx continues to be respected, but because we’re now assuming that param is a reference-to-const, there’s no longer a need for const to be deduced as part of T:
如果我们把f的形参类型从T&改为const T&,事情会有一点变化,但不会有什么特别的变化。cx和rx的const属性 仍然受到推崇,但因为我们现在假设param是 reference-to-const,所以不再需要将const推导为T的一部分:
templatevoid 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的指针)而不是引用,事情本质上将以相同的方式运行:
templatevoid 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) 规则。
例如:
templatevoid 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&&
// 个人验证结果 templatevoid f(T &¶m) // param is still passed by value { cout << "int: " << boolalpha << std::is_same ::value << endl; cout << "int&: " << boolalpha << std::is_same ::value << endl; cout << "const int&: " << boolalpha << std::is_same ::value << endl; cout << "int&&: " << boolalpha << std::is_same ::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项)解释了为什么这些例子会以这样的方式出现。
这里的重点是,通用引用形参 的类型推导规则不同于左值引用或右值引用形参的类型推导规则。
特别是,当使用全域引用时,类型推导 可以区分左值实参和右值实参。
这在非通用引用中是不会发生的。
当ParamType既不是指针也不是引用时,我们处理的是按值传递:
templatevoid f(T param); // param is now passed by value,值传递
这意味着参数将是传递内容的副本——一个全新的对象。
param将是一个新对象的事实,促使控制如何从 函数调用表达式 推导出T的规则:
- 和前面一样,如果 函数调用表达式 的类型是引用,则忽略引用部分。
- 如果在忽略了函数调用表达式 的引用性之后,表达式是const,也忽略它。如果它是volatile 关键字(和 const 对应),也忽略它。(volatile对象不常见。它们通常只用于实现设备驱动程序。具体请参见第40项。)
templatevoid 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
// 实测代码 templatevoid f(T param) // param is still passed by value { cout << "int: " << boolalpha << std::is_same ::value << endl; cout << "int&: " << boolalpha << std::is_same ::value << endl; cout << "const int&: " << boolalpha << std::is_same ::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值传递的形参:
templatevoid f(T param); // param is still passed by value const char* const ptr = "Fun with pointers"; // ptr is const pointer to const object f(ptr); // pass arg of type const char * const
// 实测代码 templatevoid f(T param) // param is still passed by value { cout << "int: " << boolalpha << std::is_same ::value << endl; cout << "const char *: " << boolalpha << std::is_same ::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 * const return 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])并不相同,但由于数组到指针的衰减规则,代码可以编译。
但是,如果一个数组被传递给一个接受按值参数的模板呢?
然后会发生什么呢?
templatevoid f(T param); // template with by-value parameter const 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来引用它的参数,
templatevoid f(T& param); // 带有引用形参的模板
我们传递一个数组给它,
const char name[] = "J. P. Briggs"; f(name); // pass array to f
// 自测代码 templatevoid 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 变量必须在编译时初始化 templateconstexpr 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::arraymappedVals; // mappedVals' size is 7
至于arraySize被声明为noexcept,这是为了帮助编译器生成更好的代码。
具体操作请参见(第14项)。
函数参数
数组并不是 C++ 中唯一可以衰减为指针的东西。 函数类型可以衰减为函数指针,我们讨论过的有关数组类型推导的所有内容都适用于函数的类型推导及其衰减为函数指针。 因此:
// someFunc is a function; type is void(int, double) void someFunc(int, double); templatevoid f1(T param); // in f1, param passed by value template 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)。
如果你已经阅读了关于模板类型推导的 第1项,那么你几乎已经了解了关于自动类型推导的所有知识,因为除了一个奇怪的例外,自动类型推导就是模板类型推导。 但怎么可能呢? 模板类型推导涉及模板和函数和参数,但 auto 不处理这些事情。
这是真的,但没关系。
模板类型推导和auto类型演绎之间存在直接映射。
从一个到另一个实际上有一个算法转换。
在第1项中,使用这个通用函数模板解释了模板类型推导
templatevoid 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// 推导x类型的概念模板 void func_for_x(T param); func_for_x(27); // 概念性调用:param推导的类型是x的类型 template // 推导cx类型的概念模板 void func_for_cx(const T param); func_for_cx(x); //概念性调用:param推导的类型是cx的类型 template // 推导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, 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
template //模板参数声明等价于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是什么:
templatevoid f(std::initializer_list initList); f({ 11, 23, 9 }); // T deduced as int, and initList's type is std::initializer_list
所以 auto 和模板类型推导之间唯一真正的区别是 auto 假定大括号初始化器表示 std::initializer_list,但模板类型推导不(模板没办法先帮{ 11, 23, 9 } 推导成std::initializer_list,导致T无法推导出来)。
您可能想知道,为什么自动类型推导对于带大括号的初始化式有特殊的规则,而模板类型推导却没有。
我自己也在想。
唉,我还没能找到一个令人信服的解释。
但规则就是规则,这意味着你必须记住,如果你使用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::vectorv; … 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 暑期编程PK赛 得CSDN机械键盘等精美礼品!



