Chapter 2 auto
Item 5:Prefer auto to explicit type declarations
关于迭代器std::iterator_traits
的详细说明
简单的总结为:std::iterator_traits
是类型特性类,为迭代器类型的属性提供统一的接口,使得能够仅针对迭代器实现算法。(如果不理解这边,可以暂时忽略,这会在其他的博文中解释)
auto
变量根据它所推断的变量或者表达式来确定自己的类型,因此auto
必须要被初始化,这可以避免忘记初始化的过程。
int x; // x如果是局部变量,随机分配。如果是全局变量或者是静态变量,默认为0
auto y; // 编译报错,auto必须得初始化,否则无法推断类型
auto z = 1.5; // 推断为double
补充说明typename的一些用法:
-
作为类型模板参数的说明,这一点与
class
作用相同。template<typename C, class E> // 声明模板C E void f(C x, E y); // 这里的C和E作用相同
-
模板中标明“内嵌依赖类型名”。参考了这篇博客。
#include <iostream> struct my_struct { int data; char ch[10]; }; class Ex { public: typedef my_struct var; // 重新给变量命名一个别名 // static my_struct var; 这是一种静态声明的方式 }; int main() { my_struct* ex = new my_struct; // 定义结构体类型 // 在这里如果不使用tpename,可能会报错。 typename Ex::var* test = ex; delete ex; test = ex = nullptr; return 0; }
在上面的代码片段中,第17行的
typename
是为了防止编译器把Ex::var
误认为是静态类型的。第10行注释掉的部分是静态声明类型,使用时和17行的方式一致。当然,不是所用的编译器都需要添加typename
关键字,比如gcc-7.2
版本就不用,但是为了兼容所有的编译器,最好是添加上typename
。 一般这种声明方式不是很常见,C++11以后出现了auto
,这样使用的机会就更少了。
不使用auto
的缺陷:
template<typename It>
void dwim(It b, It e) {
while(b != e) {
typename std::iterator_traits<It>::value_type
currValue = *b;
/*
* do some operations here.......
*/
}
}
上述函数的作用是在区间b
到e
之间,取出每个迭代器指向的内容,并进行某种操作。但是声明语句过长。
使用auto
后的优势:
template<typename It>
void dwim(It a, It b) {
while(b != a) {
auto currValue = *b;
}
}
直接 简化声明。(应该还有其他区别,但是水平所限,暂时没找到,后期更正。)
关于闭包的一些解释:(以下内容是个人翻译的,大家也可以直接看链接的wiki)
在编程语言中,闭包(又称“词法闭包”或者“函数闭包”)是一个用于实现词法作用域命名绑定的技术。在具体实现上,一个闭包是一个函数和该函数所处的环境的记录。环境指的是函数和变量的映射(变量一般是局部的,但是必须定义在一个闭合的范围之内)。闭包和普通函数不同,闭包允许函数使用那些通过闭包拷贝变量或者变量的引用的变量来捕获变量的值,即使这些函数在他们的作用域外边被调用。
举例说明:
function startAt(x)
function incrementBy(y)
return x+y
return incrementBy
variable closure1=startAt(x)
variable closure2=startAt(y)
在例子中,函数startAt
拥有一个参数x
和一个内嵌函数incrementBy
。 内嵌函数可以使用x
,因为该函数在x
的词法作用域之内,即使x
对于incrementBy
来说不是一个局部变量。函数startAt
返回了一个包含x
数值拷贝的闭包或者x
值的引用的拷贝。函数incrementBy
把y
添加到了x
上。
由于startAt
函数返回的是一个函数,因此变量closure1
和closure2
都是函数类型。调用函数closure1(3)
会返回4,调用closure2(5)
会返回8。即使closure1
和closure2
都是调用函数incrementBy
,并且调用闭包会把x
绑定到2个不同的环境中,因此函数会返回不同的数值。
闭包与匿名函数:
注意,闭包和匿名函数不是同一个概念。一个匿名函数是一个没有名字的函数,而一个闭包是函数的或者变量的实例。(这有些像类与对象)。
def f(x):
def g(x):
return x + y
return g
def h(x):
return lambda y: x + y
a = f(1)
b = h(1)
在该python例子中,a
和b
都是闭包,因为它们都是由返回内嵌函数得到的。函数f
中的内嵌函数有名字g
,而函数h
的内嵌函数没有名字。由此可以看出,闭包可以是匿名的,这种形式的闭包称为匿名闭包。
在lambda
表达式中的应用
lambda
表达式说明在后面的说明,智能指针说明在C++ primer文章说明。
对于C++函数对象的一些补充说明:
在C++ 中,可以使用一种类型的函数对象来引用任何与该类型一致的可调用的对象:
struct Ex {
int key;
};
bool cmp(const Ex& a, const Ex& b) {
return a.key < b.key;
}
int main() {
std::function<bool(const Ex&, const Ex&)>func; // func可以引用任何形如cmp的对象
func = cmp;
return 0;
}
使用auto
的lambda
函数:
class Widget {};
// 这里是C++11以及之前的标准
auto derefLess = [](const std::unique_ptr<Widget>& p1,
const std::unique_ptr<Widget>& p2)
{ return *p1 < *p2; }
class Widget {};
// C++14以及以后的标准
auto derefLess = [](const auto& p1,
const auto& p2)
{ return *p1 < *p2; }
使用函数对象声明方式的lambda
函数:
class Widget {};
std::function<(const std::unique_ptr<Widget>& p1,
const std::unique_ptr<Widget>& p2)>
derefUPless = []((const std::unique_ptr<Widget>& p1,
const std::unique_ptr<Widget>& p2))
{ return *p1 < *p2; }
出去语法上的区别,这里两种声明方式还是有很大的区别的。
auto
方式声明了一个与 目标相同的闭包,比且占用的内存空间与目标需求的一样。
std::function
方式是std::function<>
模板的实例化,对于任何目标,它只有分配一个固定大小的内存空间。如果该空间无法容纳闭包,那么std::function
会开辟堆内存来存储闭包。
综合上述,auto
方式占用空间更小,速度更快。
回避type shortcuts
以std::vector
为例,
std::vector<int>v;
unsigned sz = v.size();
auto sz1 = v.size();
在不同的操作系统中,std::vector::size()
会返回不同长度的类型,为了跨平台时更好的兼容,应该使用auto
。
for循环迭代
std::unordered_map<std::string, int>M;
for(const std::pair<const std::string, int>& p : M) {
// do some operations here
}
以unordered_map
为例,由于std::unordered_map
的第一个是常类型,所以编译器会把std::pair<const std::string, int>
转化成std::pair< std::string, int>
类型。因此。编译器会创建临时变量用于类型转换,在结束时销毁临时变量。这回造成时间和空间的额外开销。
std::unordered_map<std::string, int>M;
for(const std::pair<const auto& p : M) {
// do some operations here
}
这种方式会直接把p
绑定到M
的数据中,高效而且方便。进一步说,如果我们想直接获取M
中元素的地址,可以直接调用p
,p
的地址就是代表M
中的地址。
一些注意事项:
auto
是一个操作,而不是一个类型。如果auto
绑定的初始化类型发生变化,auto
操作的类型会自动转换自己类型与之匹配,因此如果代码要改动的话,会自动进行的。
总结:
auto
类型必须被初始化auto
可以有效处理由于类型不匹配导致的可移植性或者效率问题。- 可以延缓重构,并简化代码,方便阅读。
##Item 6:Use the explicitly typed initializer idiom when auto deduces undesired types
class Widget {/*一些特性*/};
std::vector<bool> features(const Widget& w);// 定义优先级函数
bool highPriority = features(w)[5]; // 获取优先级
processWidget(w, highPriority); // 根据优先级,进行有关的处理
假设Widget
拥有多个特性,features
函数返回Widget
的每个特性是否还在保持,且bit 5表示是否有高的优先级。上述操作是正确的。
auto hightPriority = features(w)[5]; // 获取优先级
processWidget(w, highPriority); // 根据优先级,进行有关的处理
**第2行的操作是错误的!!!!!**在这里,highPriority
不再是bool
类型。尽管在概念上,std::vector<bool>
仍然返回bool
类型; 但是对于``std::vector 的运算符
operator[]来说,**不返回容器元素的引用!!!!!** 这也可以理解成
std::vector::operator[]除了
bool类型之外,其余的都正常返回。
operator[]真正返回的是
std::vector::reference类型,这是一个内嵌在
std::vector`中的类。
参见这篇博客和知乎上这个问题 ,总结一句话:std::vector<bool>::reference
不是bool
类型,实际上是一个bit位,而且C++不允许对bit位进行引用!!!
在实际的操作中,这种使用方式会发生一次隐式类型转换到bool
,但一定不是bool&
。但是如果全部解释的话,需要的篇幅过多,这会在其他的博文中进行论述。
因此,在这里再次回顾代码:
bool highPriority = features(w)[5];
这里,features
返回std::vector<bool>
类型;operator[]
被调用,之后返回std::vector<bool>::reference
类型,同时,之后该类型被隐式地转换成bool
类型去初始化highPriority
。highPriority
因此会获取std::vector<bool>
被函数features
返回的bit 5类型,正如我们预想的那样。
对比使用auto
的代码:
auto hightPriority = features(w)[5];
同样的,features
返回std::vector<bool>
类型,operator[]
被调用,之后返回std::vector<bool>::reference
类型。但是,这里发生了一些变化,highPriority
不会对features
返回的进行强制类型转换。因此highPriority
不会再继续是bit 5的bool
值了,它的值依赖于std::vector<bool>::reference
的具体实现方式,std::vector<bool>::reference
不再是features
返回的std::vector<bool>
的bit 5了。
对于这种的类型,一种实现方式是使用一个指针来指向含有bit引用的机器字,并且在该机器字上添加一个偏移量。调用features
会返回一个临时的std::vector<bool>
对象,假设该对象名字为temp
。 操作operator[]
会调用temp
,std::vector<bool>::reference
会返回一个被temp
管理的机器字,并且该机器字会添加上某个位移来指向bit 5。 highPriority
是一个std::vector<bool>::reference
对象的一个复制,因此,highPriority
也包含了一个指向temp
中机器字的指针和该机器字的某个为了适应指向bit 5的偏移量。该语句结束后,temp
被销毁,因为这是一个临时的对象。因此,highPriority
包含了一个悬空的指针,并且就是该悬空的指针造成了processWidget(w, highPriority)
的错误。
std::vector<bool>::reference
是代理类的一个实例:一个模仿和增加其他类功能的类。std::vector<bool>::reference
是为了提供operator[]
返回std::vector<bool>::reference
的一个假象;并且STL
的智能指针也是为了向原生指针添加资源管理功能的代理类。代理类的功能是固定下来的,实际上,设计模式中的代理类是最早出现的软件设计模式中的成员之一。
一些代理类的设计是为了更好的向用户展示功能或者更方便使用,比如std::share_ptr
和std::unique_ptr
。
另一些代理类的设计是为了更多的功能或更少的可见性。std::vector<bool>::reference
就是一个可见性代理的例子。
在一些C++的库中,代理类被用作一种名为“表达式模板”的技术。这些库最初是用来提高数值计算代码的效率。给出一个矩阵计算的例子,表达式:
Matrix sum = m1 + m2 + m3 + m4;
可以被更高效的计算,如果矩阵运算符operator+
被设计成返回一个结果的代理类而不是结果的本身。也就是说,对于两个矩阵运算的operator+
将会返回一个形如Sum<Matrix,Matrix>
而不是Matrix
的对象。就像之前的例子std::vector<bool>::reference
和bool
, 这里有一个从代理类向Matrix
的隐式的类型转换,这将会允许Sum
直接从代理对象进行初始化,代理对象产生在=
右侧的表达式。可以换一种方式来理解,传统的初始化表达式可能会写成:
Sum<Sum<Sum<Matrix, Matrix>, Matrix>, Matrix>;
的形式。这种类型的定义方式应该会被用户避开。
“不可见”代理类与auto
的兼容性不是特别好。这种类的对象的生存周期一般设计的比单个实例短,因此创造这种类型的变量违反了基本类库的设计假设,比如std::vector::reference
的例子,而且这种违背基本假设例子会导致undefined behavior。
因此,我们应该避免使用:
auto someVar = expression of "incisible" proxy class type
综合上述,auto
与代理类的真正的矛盾在于auto
把类型推断成了代理类的类型,而不是推断成我们想要的被代理的类型。auto
本身并没有错误,错的是推断类型不是我们想要的。解决方案是:强制使用一个不同类型的推断。这种方式称为explicitly typed initializer idiom
使用方式:
auto highPriority = static_cast<bool>(features(w)[5]);
这里,features(w)[5]
仍然返回std::vector<bool>::reference
对象,但是cast
把表达式的类型转换成bool
的类型,之后auto
再推断highPriority
的类型。在运行时,std::vector<bool>::operator[]
返回返回的std::vector<bool>::reference
对象执行它所支持的向bool
类型转换; 作为转换的一部分,从features
返回的、指向std::vector<bool>
的 still-valid
指针被推断。这将会阻止undefined behavior
对于之前矩阵的实例,在这里应该这样使用:
auto sum = static_cast<Matrix>(m1 + m2 + m3 + m4);
idiom的使用不会被初始化列举的代理类类型所限制。它对强调你可以创造的变量类型也很有用,该类型与初始化表达式产生的类型是不同的。
比如,我们有一个函数来计算一些公差数值:
double calcEpsilon();
很明显calcEpsilon
返回double
类型,但是假设你知道对你的应用来说,float
的精度足够了,并且你比较在及float
和double
所占用的空间。你可以声明一个float
变量来存储calcEpsilon
的结果:
float ep = calcEpsilon(); // 隐式地从double转换到float
一种显式的声明方式为:
auto ep = static_cast<float>(calcEpsilon());
总结:
- 不可见的“代理类型”会使
auto
从初始化表达式推断错误,要尽量避免这种给类型。 - 显式的类型初始化会强制
auto
推断我们想要的类型。