Effective Modern C++ 第二章 auto的使用

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的一些用法:

  1. 作为类型模板参数的说明,这一点与class作用相同。

    template<typename C, class E>  // 声明模板C E
    void f(C x, E y);              // 这里的C和E作用相同
    

  2. 模板中标明“内嵌依赖类型名”。参考了这篇博客

    #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.......
        */
    }
}

上述函数的作用是在区间be之间,取出每个迭代器指向的内容,并进行某种操作。但是声明语句过长。

使用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值的引用的拷贝。函数incrementByy添加到了x上。

由于startAt函数返回的是一个函数,因此变量closure1closure2都是函数类型。调用函数closure1(3)会返回4,调用closure2(5)会返回8。即使closure1closure2都是调用函数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例子中,ab都是闭包,因为它们都是由返回内嵌函数得到的。函数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;
}

使用autolambda函数:

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中元素的地址,可以直接调用pp的地址就是代表M中的地址。

一些注意事项:

auto是一个操作,而不是一个类型。如果auto 绑定的初始化类型发生变化,auto操作的类型会自动转换自己类型与之匹配,因此如果代码要改动的话,会自动进行的。

总结:

  1. auto类型必须被初始化
  2. auto可以有效处理由于类型不匹配导致的可移植性或者效率问题。
  3. 可以延缓重构,并简化代码,方便阅读。

##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类型去初始化highPriorityhighPriority因此会获取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[]会调用tempstd::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_ptrstd::unique_ptr

另一些代理类的设计是为了更多的功能或更少的可见性。std::vector<bool>::reference就是一个可见性代理的例子。

在一些C++的库中,代理类被用作一种名为“表达式模板”的技术。这些库最初是用来提高数值计算代码的效率。给出一个矩阵计算的例子,表达式:

Matrix sum = m1 + m2 + m3 + m4;

可以被更高效的计算,如果矩阵运算符operator+被设计成返回一个结果的代理类而不是结果的本身。也就是说,对于两个矩阵运算的operator+将会返回一个形如Sum<Matrix,Matrix>而不是Matrix的对象。就像之前的例子std::vector<bool>::referencebool, 这里有一个从代理类向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的精度足够了,并且你比较在及floatdouble所占用的空间。你可以声明一个float变量来存储calcEpsilon的结果:

float ep = calcEpsilon();  // 隐式地从double转换到float

一种显式的声明方式为:

auto ep = static_cast<float>(calcEpsilon());

总结:

  1. 不可见的“代理类型”会使auto从初始化表达式推断错误,要尽量避免这种给类型。
  2. 显式的类型初始化会强制auto推断我们想要的类型。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Effective Modern C++是一本由Scott Meyers所著的C++编程指南,该书针对C++11和C++14进行了全面介绍和分析。它是《Effective C++》的续作,适用于现代C++编程。对于C++开发者来说,它是一本非常宝贵的参考书。 PDF网盘是指网络上的一种存储和分享文件的服务平台。用户可以将文件上传到该平台,并与他人共享。这种服务通常提供了可在线浏览和下载文件的功能。 "Effective Modern C++ PDF网盘"这个问题的含义可能是在寻找《Effective Modern C++》这本书的PDF版本,并将其存储在PDF网盘上以供下载和分享。 寻找《Effective Modern C++》的PDF版本可以在搜索引擎上进行查找。一般来说,用户可以在一些知名的在线书店或学术资源网站上找到免费或付费的PDF版本。找到合适的文件后,用户可以将其上传到选择的PDF网盘,如Google Drive、百度云、Dropbox等。 将《Effective Modern C++》上传到PDF网盘上的好处是可以将文件与他人共享,并提供在线浏览和下载的功能。这样其他人可以方便地获取这本书的电子版本,而不必寻找纸质书或购买电子书。此外,PDF文件的格式保留了原始书本的版面结构,可以在不同设备上方便地阅读和浏览。 总之,《Effective Modern C++》是C++开发者的一本重要书籍,而PDF网盘是一个方便存储和分享文件的网络服务平台。将这本书的PDF版本上传到PDF网盘上,可以方便他人获取并进行阅读和学习。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值