(莫寒C++学习之路)深入实践C++模板编程读书笔记01序章

27 篇文章 0 订阅
3 篇文章 0 订阅

概览

这里是通过记录学习的纸质书籍《深入实践C++模板编程》,因此放上华章的连接:

《深入实践C++模板编程》

在我们日常学习C++的过程中,其实不难发现该语言是一门强类型的语言,是一个对于面向对象编程支持的非常好的语言。因此在强类型的要求下,如果只是单纯地遵守**“万物皆对象”**的法则,那么必然会出现同一种操作/算法在不同的对象里大量的重复实现的过程,或者有的程序员会采用,设计一个抽象的类型来专门设计容纳一些通用的操作和算法。

但是,不然是后者高级的设计,对于大量的对象来说,工作起来依然是看起来捉襟见肘的,那么对于一个C++程序员来讲,是否理解模板编程的原理以及是否掌握模板编程的方法是普通C++程序员与高级C++程序员的分水岭,今天我在此通过阅读《深入实践C++模板编程.温宇杰》本书,记录相关的学习过程,深入理解C++模板编程,进一步提升自己的编程能力,革新本人的C++编程思想。

该书主要包含以下知识点:

  • 函数模板的原理、用法、模板参数自动推导,以及函数体的实现方法;
  • 外名模板极其原理;
  • 类模板及其用法,异质链表、元组构造方法,以及类模板的静态变量成员的处理;
  • 各种整数型、指针型以及引用型模板参数类型详解;
  • 模板的特例、函数模板的重载、特例的写法以及匹配规则;
  • 标准库中的容器、迭代器和算法的定义、作用以及他们之间的关系
  • 标准库中的序列型容器、关联型容器、散列表容器、C++11新增的容器等多种类型容器的实现原理;
  • 多种分配器和迭代器的实现原理极其编写方法;
  • 标准库中常用算法的原理分析及其应用实践;
  • 模板编程的常用技巧和元编程技术;
  • C++11新标准的模板新特性和新语法,以及C++11新标准中新增的语言特性解读。

作者写书的初衷

作者在工作实践中,几番研究与实践下来,作者尚不敢称对C++的模板编程掌握几分,却早已叹于其对作者编程思想革新起到的巨大的帮助。从“万物皆对象”,到如今渐渐变为关注设计容器以及抽象算法的过程,这是拜模板所赐。赞叹之余,不禁想尝试写一部书介绍C++中的模板编程,与人分享心得,于己则巩固琢磨。不论章节条理,只求为位到来。如果对读者略有裨益,便是幸甚。

其实,很多程序员对于分享自己的见解和认识都还是比较热衷的,相互学习各种思想,让自己对于技术有更大的提升和思考。对于书中的技术和见解,像作者希望的那样,希望能够做到“择其善者而从之,其不善者而改之”。

模板基础

本部分的内容有:

  • Hello 模板
  • 类亦模板
  • 模板参数类型详解
  • 凡事总有“特例”

Hello 模板

C++可以被称为强类型语言,凡是值必有类型,凡是变量声明时必须声明其类型,且变量类型“一生”不变。这样就出现了,对于某些算法,针对不同数据其操作过程完全一致,只因为操作数据类型不同,在C++中需实现为不同的函数,难免重复了。

为什么需要使用模板

试想一个寻找最大值的案例,从一列值里面计算最大的值的操作,用Python这种动态类型的语言来做,算法代码就非常的简洁:

def max_element(l):
	max_value = l[0]
	for elem in l[1:]if elem > max_value: max_value = elem
	return max_value
print(max_element([2, 0, 1, 1, 0, 8, 1, 8]))
print(max_element(['2011', 'August', '11', 'Thursday']))

Python实现上面的代码,不需要操心类型的前提下,会发现实现是非常简单的。但是,使用C++来实现一个通用的max_element函数就没有那么简单了,C++中变量的类型不可变,不能像Python那样用同一个函数、同一个变量处理不同的列表类型。如果没有使用C++的模板类型的话,对于整数数组,代码如下:

int max_element(int const *1, unsigned sz)
(
	int max_value = 1[0];
	for (unsigned i = 1; i < sz; ++i)
		if (1 [i] > max_value) max_value = 1[i];
	return max_value;
} 

对于字符数组,则函数代码就改为:

char max_element(char const *1, unsigned sz)
{
	char max_value = 1[0];
	for (unsigned i = 1; i < sz; ++i)
		if (1[i] > max_value) max_value = 1[i];
	return max_value;
}

会发现,除了函数返回值、第一个函数参数类型有变化之外,其他的内容基本是完全一摸一样的。因此使用函数模板进行简化就是一个值得学习的思路和方法。因此,我们与模板的第一次亲密接触就应该从模板函数开始了。函数模板是C++模板机制中的一种,其作用是为不同类型的数据生成操作相同或相似的函数。

模板以关键字template开头,其后是以一对尖括号划分的模板参数列表。模板参数列 表中可以声明多个模板参数,多个参数声明之间以逗号分隔。使用函数模板实现就是以下的代码:

template<typename T>
T const& max_element(T const *l, unsigned sz)
{
    T const *max_value(l);
    for (unsigned i = 1; i < sz; ++i)
        if (l[i] > *max_value) max_value = &(l[i]);
    return *max_value;
}

上面的例子中函数模板的函数体与前面两份的函数代码非常相似,不同之处在于凡是有关列表类型之处,皆由模板类型参数T代替。 另外,考虑到列表元素可能为复杂自定义类型,其赋值会导致额外开销,在模板中将max_ value改为指针以避免无谓赋值。

那么,就可以使用函数模板的过程来模拟实现上面两个代码的过程了:

int main()
{
    int l[] = {2, 0, 1, 1, 0, 8, 2, 5};
    char cl[] = "August";

    using namespace std;
    cout << max_element<int>(l, 8) << endl;
    cout << max_element<char>(cl, 6) << endl;

    return 0;
}

可以像调用一个普通函数那样调用函数模板。不同之处在于,调用函数模板时需要指明模板参数的“值”。对于类型参数,其“值”即为具体类型如int、char或者是用户自定义的类。根据所给定的模板参数值以及完整的函数模板声明,编译器可自动生成一个对所需数据类型进行操作的函数,称为函数模板实例。模板参数的值在函数模板名后接尖括号内声明。

模板参数自动推导

实际上,在C++语言中实现了这一自动推导模板参数值的功能。凡是可以推导出的模板参数"值”,就无需在模板实参列表中写明。因此,例1.4中main函数的两次max_ element调用,可以简写成以下形式:

std::cout << max_element(1, 8) << std::endl;
std::cout << max_element(cl, 6) << std::endl;

从而使得模板调用看起来与普通函数调用无异,也使代码看起来更整洁。
利用模板参数推导时需要注意以下几点:

  • 编译器只根据函数调用时给出的实参列表来推导模板参数值,与函数参数类型无关 的模板参数其值无法推导。
  • 与函数返回值相关的模板参数其值也无法推导。
  • 所有可推导模板参数必须是连续位于模板参数列表尾部,中间不能有不可推导的模 板参数。

下面是一个简单的多模板参数的函数使用案例:

#include <iostream>

template<typename T0,
         typename T1,
         typename T2,
         typename T3,
         typename T4>
T2 func(T1 v1, T3 v3, T4 v4);

int main() {

    double sv2;

    using namespace std;
    sv2 = func<double, int, int>(1, 2, 3);
    cout << "\tsv2: " << sv2 << endl;

    sv2 = func<double, int, int>(1, 2, 3);
    cout << "\tsv2: " << sv2 << endl;

    sv2 = func<double, int, int>(1, 0.1, 0.1);
    cout << "\tsv2: " << sv2 << endl;

    sv2 = func<int, double, double>(0.1, 0.1, 0.1);
    cout << "\tsv2: " << sv2 << endl;
}

template<typename T0,
         typename T1,
         typename T2,
         typename T3,
         typename T4>
T2 func(T1 v1, T3 v3, T4 v4)
{
    T0 static sv0 = T0(0);
    T2 static sv2 = T2(0);

    std::cout << "\tv1: " << v1
              << "\tv3: " << v3
              << "\tv4: " << v4
              << "\t|| sv0: " << sv0;
    T2 v2 = sv2;

    sv0 -= 1;
    sv2 -= 1;

    return v2;
}

模板实参列表中只可将T3及T4省略,而T0、T1及T2 不能省略。

模板参数默认值

最新的C++11标准允许为函数模板参数赋默认值,在为func中无法根据函数参数推导的模板参数赋予默认值后,调用模板时的模板实参列表可以完全省略。例如将func的声明改为以下形式:

tempiate<	typename	T0	= float,
			typename	Tl,	
			typename	T2	= float,
			typename	T3,	
			typename	T4>	
T0 func (T1 vl, T3	v3,	T4 v4)

如何处理函数模板中的函数体

既然编译器是在需要生成模板实例时自动生成,这就带来一个与传统C/C++编程习惯 的冲突,即函数模板中的函数体应该放在哪里。

hpp文件还是cpp文件

按照C++语言习惯,普通函数及类的声明应该放在一个头文件(通常以h、hpp或者 hh为扩展名)里,而将其实现放在一个主代码文件(通常以c、cpp或者cc为扩展名)里,这样便于将代码分散编译到多个目标文件中,最后通过链接形成一个完整的目标文件。但是由于模板的实现是随用随生成,并不存在真实的函数实现代码,如果还是按照“头文件放声明,主文件放实现”的做法,则会导致编译失败。

template<typename T>
T const& func(T const &v) {return v;}

template int const& func(int const &v);

例中用到一种尚未介绍过的语法——明确生成模板实例。当关键字template后没有模板参数列表,而是一个函数声明时,意味着指示编译器根据此函数声明寻找合适的模板实现。当然,所声明函数必须与某一已知模板函数同名,并且其参数可用模板匹配。

例中将函数声明为T=int,从而在编译func2.cpp时,会在目标文件中生成 func< int >的代码而不会在链接时产生错误。但这只是权宜之计,倘若还需要func< float > 或 者func< char >,那么在代码文件中还得增加相应的语句,以促使编译器生成相应函数模板实例。如此一来, 又变成由人工生成模板实例,违背了当初由编译器随用随生成的初衷。

可见,虽然模板中的函数也可以有自己的声明和实现,但编译器不会在读到模板实现时立刻生成实际代码,因为具体的模板参数类型还未知,无法进行编译。对于编译器来说, 模板实现也是一种声明,声明如何自动生成代码。所以模板的实现也应该放在头文件内, 这样,在其他代码文件中可以直接将模板的实现也包含进来,当需要生成模板实例时,编译器可根据已知模板实现当场生成,而无需依赖在别的目标文件中生成的模板实例。

但这样会带来另一个问题,即重复模板实例。对此问题,C++标准中给出的解决方案是:在链接时识别及合并等价的模板实例。

caller1.cpp

//======================================
//文件名caller1.cpp
#include <iostream>

template<typename T>
void func(T const &v)
{
    std::cout << "func1: " << v << std::endl;
}

void caller1() {
    func(1);
    func(0.1);
}


caller2.cpp

//======================================
//文件名caller2.cpp
#include <iostream>

template<typename T>
void func(T const &v)
{
    std::cout << "func2: " << v << std::endl;
}

void caller2() {
    func(2);
    func(0.2f);
}

main.cpp

//======================================
//文件名main.cpp
void caller1();
void caller2();

int main()
{
    caller1();
    caller2();
    return 0;
}

这两个目标文件再与main.cpp编译所得目标文件共同链接成可执行文件后会出现什么情况呢?
执行结果如下:

$ ./a.out
func1 : 1
func1: 0.1
func1: 2
func2: 0.2

由此例的运行结果可以推知,链接器不考虑函数具体内容,仅仅通过函数名模板实参列表以及**参数列表等“接口”**信息来判断两个函数是否等价。

常情况下,根据函数接口判断等价函数实例并在链接时合并的简单方法,可以有效解决重复模板实例的问题。但正如例中所演示那样,使用这种方法也有弊端。倘若有不同的作者在写不同的模板库时,碰巧用到同一函数名以及相同的模板参数列表和函数形参列表,对于一些简单函数,这也是非常有可能的。

降低落入这一陷阱的可能性,最好的方法就是避免使用相同的函数名。此时,C++中的命名空间(namespace)机制就显得异常重要。

尴尬的 Export Template

实际上,除了将模板的声明与实现一同放在头文件中编译之外,C++标准(98版)还 提供了另一种组织模板代码的方式——Export Template(暂且称为外名模板)。而定义与实现放在同一头文件中的模板可称为内名模板

外名模板提倡将模板的声明与实现分别写在头文件和主文件内,如此实现模板需要在模板声明前加关键字export以标记外名模板,可以看如下代码:

square.hpp

//======================================
//文件名:square.hpp
export template<typename T>
T square(T const &v);

square.cpp

//======================================
//文件名:square.cpp
#include "square.hpp"
export template<typename T>
T square(T const &v) {return v * v;}

main.cpp

//======================================
//文件名:main.cpp
#include "square.hpp"
#include <iostream>

int main()
{
    std::cout << square(0.1f) << std::endl;
}

而在链接时,会因为无法找到main.cpp中 所需要的square< float >模板实例而报错。例如用GCC编译时情况大抵如此,并且GCC还会给出一个“不支持export”的警告。

由于实现成本高,而且需求可替代,使得外名模板不受编译器开发者的青睐并且最终 被逐出最新的C++11标准。虽然如此,这样一种设计思想仍然值得了解,并且现在也有完全支持外名模板的编译器可供使用。对于看重编译速度及目标码质量而不太看重代码移植 性的开发者来说,外名模板仍然是一种不错的选择。

本章小结

本章所讨论的只是C++模板编程中的一小部分——函数模板而已。通过对函数模板编译过程的介绍,希望能对读者理解模板的本质有所帮助。

模板本身不是可编译的代码,而是用来指导编译器生成可编译代码的文本。函数模板 实际上是提取一系列具有相同(或者近似)操作流程的函数中的共性并给予规范描述,从而使得编译器可以在需要时,根据描述自动生成相应可编译代码并编译。有了模板的支持, 程序员的工作从体力劳动向着脑力劳动又前进了一小步。但是如果仅仅是函数模板而已, 这一小步并不会走岀太远,仍然有许多问题是单纯用函数模板无法解决的,另一小步便是第2章将要介绍的类模板及其功用。

在这里插入图片描述

成长,就是一个不动声色的过程,一个人熬过一些苦,才能无所不能。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值