详解C++ 模板编程

类型模板

类型模板包括函数模板和类模板,基本上是C++开发人员接触模板编程的起点。

下面代码演示了函数模板和类模板的使用方法:

// 函数模板
template<typename T>
T add(const T& a, const T& b) {
 return a + b;
}

// 类模板
template<typename T>
class Point {
private:
 T x[3];
 ...
};

类型模板以template开始声明,尖括号内的typename关键字可用class替代。类型模板中typenameclass具有相同含义,均表示参数类型。实践中typename语义更广泛,表示其后续的参数T是一个类型,不限定于类,建议使用。类型参数T可换成其他任意有意义的合法变量。

C++14新增变量模板:

// 变量模板
template<tyepename T>
constexpr T pi = T(3.1415926535897932385L);

尖括号之于模板犹如小括号之于函数:函数通过小括号()定义和调用,模板使用尖括号<>定义(需template关键字声明)和实例化。上面演示了类型模板定义,下面代码介绍模板实例化:

int a = 1, b = 2;
// 实例化函数模板
std::cout << "add result:" << add<int>(a, b) << std::endl;

// 实例化类模板
auto p = Point<int>();

double radius = .5;
// 实例化变量模板
auto area = pi<double> * radius * radius;

同函数一样,模板可以有默认值:

// 默认类型为int
template<typename T=int>
T add(const T& a, const T& b) {
 return a + b;
}

// 默认类型为double
template<typename T=double>
class Point {
private:
 T x[3];
 ...
};

与函数不同,对于函数模板,如果能从参数推断出模板类型,则可略去尖括号模板实例化参数:

int a = 1, b = 2;
// 合法调用,编译器能根据a b推断出参数类型
std::cout << "add result:" << add(a, b) << std::endl;
// 等同于
std::cout << "add result:" << add<int>(a, b) << std::endl;

然而对于类模板,即使有默认参数,也不能省略尖括号(但是可以省去参数):

template<typename T=double>
struct Point {
 T x[3];
};

// 合法声明
auto p = Point<double>();
// 合法声明,类型使用默认的double
auto p2 = Point<>();
// 非法声明,缺少模板调用标志尖括号
auto p3 = Point();

类型参数模板在实际中使用最多,STL库中vector、map等容器、algorithm中的许多算法都用到了模板。

非类型参数模板

另一类常用模板是非参数模板,用来替代某个具体的值。例如:

// N维空间向量
template<int N>
struct Vector {
 double x[N];
};

// 实例化
auto v = Vector<100>();
...其他操作

需要注意的是,非类型参数模板能使用的类型十分有限,只有(signed/unsigned)整数、char和枚举这几种类型可用(参考switch语法)。

同类型模板一样,非类型参数模板也可以有默认值,但应用到类模板实例化也不能省略尖括号。

类型模板和非类型参数模板可以结合一起用:

template<typename T, int N>
struct Point {
 T x[N];
};

类型模板解决了类型问题,非类型参数模板解决了值的问题,实际中应用也十分广泛。作为递归的经典场景,斐波那契数列可以用非类型模板解决:

template<int N>
struct Fib {
 static constexpr int value = Fib<N-1>::value + Fib<N-2>::value;
};
// 模板特化
template<>
struct Fib<1> {
 static constexpr int value = 1;
};
// 模板特化
template<>
struct Fib<0> {
 static constexpr int value = 0;
};

// 调用
std::cout << "Fib(10): " << Fib<10>::value << std::endl;

这个例子出现了”模板特化”,接下来介绍。

模板特化/偏特化

定义模板后,希望在特定条件下使用单独的模板,这便是模板特化。上文中斐波那契数列定义的template<int N> struct Fib是母模板,接下来又定义了0和1两个特化模板(子模板),指示编译器遇到Fib<0>和Fib<1>的情况,使用这两组单独定义。需要注意的是特化模板的template参数为空,具体模板参数放到了模板名称处,类似于模板实例化。

对多个模板参数的情形,如果只特化某个模板参数,便是偏特化。例如:

// 泛型模板定义
template<typename T1, typename T2> struct Add;
// 特化模板
template<> struct Add<int, int> {...};
// 偏特化模板
template<typename T> struct Add<T, long> {....};

模板特化/偏特化类似于函数重载,能针对特殊情况进行特别处理。

模板匹配与SFINAE

模板特化使得同一个模板名称有了多个定义,代码具体调用时会遇到模板匹配问题。理解模板匹配机制的关键便是SFINAE,这也是进阶模板编程的必备知识点。

SFINAE是Substitution failure is not an error的缩写,翻译过来便是:匹配(替换)失败不是错误。

怎么理解这句话呢?

对于上面的斐波那契数列数列代码,编译器遇到Fib<10>::value的代码,(可能)先会尝试匹配Fib<0>,发现匹配不上,这是一个Substitution failure,但不是error,所以编译器继续尝试其他可能性。接着匹配Fib<1>,同样发现匹配不上,忽略这个Substitution failure继续尝试Fib<N>,OK,这一次没问题,编译成功。

如果是Fib<-1>::value,编译器达到最大递归深度也找不到一个合适的匹配模板,这是一个error,因此编译失败。

备注:理解上面的话需要对编译过程稍加了解,编译过程会输出许多信息,编译器一般只有遇到error才会终止编译,比较常见的warning则不会。模板匹配中的Substitution可能连warning都算不上,不会影响编译器继续尝试匹配

理解SFINAE是看懂稍微深奥点模板代码的基本功,重点便是:不怕你模板多,就怕找不到合适的模板。

两阶段编译

有了模板(元)编程,C++源码编译可以分为前期和后期,构成两阶段编译。前期是模板的天下,编译器扫描模板实例化语句,生成运算结果和具体代码;后期编译器介入,再编译生成机器码。

模板代码运行在编译期,因此有如下特点:

  • 没有实例化的模板代码,即使有语法错误,编译器也不会检查和报错。对按代码行数考核KPI的C++码农,这绝对是福音,新增template代码十万行,瞎编乱写都可以,只要不实例化,永远能编译通过,编译后的文件大小(一般)不变,也不影响现有代码运行;
  • 对于常量,编译前期直接计算,没有运行时开销。上文中的斐波那契数列值在编译期便已经计算出来了;
  • 无法运行期动态调用代码。例如下面的要求做不到:
template<int N>
struct Point {double x[N];};
// 根据输入动态生成类,无法实现和编译成功
int n;
std::cin >> n;
auto p = new Point<n>();
  • 模板和多态/虚函数(理念)冲突。多态/虚函数的关键是运行期动态调用代码,而模板在编译期确定,因此两者理念上是冲突的。所以,如果你想一个成员函数既是模板函数,又是虚函数,怎么做实现预期?

C/C++编译有个预处理过程,只是做简单字符串替换,没有具体运算,与模板生成代码不同

在编译前期,除了模板代码被解释执行,其他代码信息都在,因此模板代码拥有类似反射/自省的能力,这也是C++元编程功能强大的原因之一。

C++11中的变化

C++11带来了许多新特性和重大更新,可以认为C++11是一门新的语言。就模板来说,主要更新点如下:

1. 可以使用static constexpr int代替早期模板代码中的enum。网上许多斐波那契数列代码都是基于早期C++,一律使用enum方式定义字段;

2. 可以使用using代替typedef。这是using语句能力的重大更新,早期我们定义类型或者别名都需要typedef,自C++11开始,简单使用using就可以达到相同效果。

3. C++14引入了变量模板,上文已介绍。

模板优缺点

上文根据自己理解和实践简要介绍了C++模板编程的相关概念,本节总结一下C++模板的优缺点:

C++模板编程优点:

  • 减少代码输入,提高代码重用和编程效率;
  • 支持鸭子类型(Duck typing)的特性,使用便利,功能强大;
  • 某些情况下能减少运行期开销;
  • 能实现元编程,C++高手必备之路;

C++模板编程缺点:

  • 语法看起来是hack黑科技,代码可读性差,编写繁琐;
  • 模板代码调试困难,生成的错误信息也晦涩难懂。你可以还记得刚开始使用STL模板的map等数据类型报错的恐怖提示?
  • 编译时间增加。

感谢阅读,欢迎指正!

以上就是详解C++ 模板编程的详细内容,更多关于C++ 模板编程的资料请关注我们其它相关文章!

(0)

相关推荐

  • C++设计模式编程中Template Method模板方法模式的运用

    准备一个抽象类,将部分逻辑以具体方法以及具体构造子的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑.不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现.这就是模版方法模式的用意. 很多人可能没有想到,模版方法模式实际上是所有模式中最为常见的几个模式之一,而且很多人可能使用过模版方法模式而没有意识到自己已经使用了这个模式.模版方法模式是基于继承的代码复用的基本技术,模版方法模式的结构和用法也是面向对象设计的核心. 模版方法模式需要开发抽象类和具体子类的设计师之间的协作

  • 详解C++编程中类模板的相关使用知识

    有时,有两个或多个类,其功能是相同的,仅仅是数据类型不同,如下面语句声明了一个类: class Compare_int { public : Compare(int a,int b) { x=a; y=b; } int max( ) { return (x>y)?x:y; } int min( ) { return (x<y)?x:y; } private : int x,y; }; 其作用是对两个整数作比较,可以通过调用成员函数max和min得到两个整数中的大者和小者. 如果想对两个浮点数(

  • 解读C++编程中类模板的三种特化

    1.类模板显式特化 为了进行特化,首先需要一个通用的版本,称主模板.主模板使用了标准库堆算法.  堆 是一种线性化的树形结构,将一个值压入一个堆中, 实际上等于将该值插入到一个树形结构中;将一个值从堆中取出就等于移除并返回堆中最大值.但在处理字符的指针时会碰钉子.堆将按照指针的值进行组织. 我们可以提供一个显式特化版本解决此问题(例1)如果希望除了一个针对const char*的Heap外,还希望提供一个针对char *的Heap;(例2) //主模板 template <typename T>

  • 浅谈C++模板元编程

    所谓元编程就是编写直接生成或操纵程序的程序,C++ 模板给 C++ 语言提供了元编程的能力,模板使 C++ 编程变得异常灵活,能实现很多高级动态语言才有的特性(语法上可能比较丑陋,一些历史原因见下文).模板元编程的根在模板.模板的使命很简单:为自动代码生成提供方便.提高程序员生产率的一个非常有效的方法就是"代码复用",而面向对象很重要的一个贡献就是通过内部紧耦合和外部松耦合将"思想"转化成一个一个容易复用的"概念".但是面向对象提供的工具箱里面所

  • 详解C++ 模板编程

    类型模板 类型模板包括函数模板和类模板,基本上是C++开发人员接触模板编程的起点. 下面代码演示了函数模板和类模板的使用方法: // 函数模板 template<typename T> T add(const T& a, const T& b) { return a + b; } // 类模板 template<typename T> class Point { private: T x[3]; ... }; 类型模板以template开始声明,尖括号内的typen

  • 详解C++模板编程中typename用法

    typename的常规用法 typename在C++类模板或者函数模板中经常使用的关键字,此时作用和class相同,只是定义模板参数:在下面的例子中,该函数实现泛型交换数据,即交换两个数据的内容,数据的类型由_Tp决定. template <typename _Tp> inline void swap(_Tp& __a, _Tp& __b) { _Tp __tmp = __a; __a = __b; __b = __tmp; } typename的第二个用法:修饰类型 限定名和

  • 详解velocity模板使javaWeb的html+js实现模块化

    详解velocity模板使javaWeb的html+js实现模块化 页面上一些基础数据或者其他页面经常用到部分,可以独立出来做成小组件,组件预留调用入口,需要的页面直接调用即可. 如图,页面中的展示分类和搜索标签在多个页面重复使用,可以将这部分内容独立出来,做成组件,供后续开发调用: classify_search_tag.html文件如下,其中包含HTML节点和jQuery代码: <!-- 展示分类与搜索标签组件使用说明: 1.新增时父页面调用方法:页面加载时调用 goodsClassifyA

  • JavaScript实现AOP详解(面向切面编程,装饰者模式)

    什么是AOP? AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计.安全控制.异常处理等.把这些功能抽离出来之后, 再通过"动态织入"的方式掺入业务逻辑模块中. AOP能给我们带来什么好处? AOP的好处首先是可以保持业务逻辑模块的纯净和高内聚性,其次是可以很方便地复用日志统计等功能模块. JavaScript实现AOP的思路? 通常,在 JavaScript 中实现 AOP,都是指把一个函数"动态织入&qu

  • 详解C#多线程编程之进程与线程

    一. 进程 简单来说,进程是对资源的抽象,是资源的容器,在传统操作系统中,进程是资源分配的基本单位,而且是执行的基本单位,进程支持并发执行,因为每个进程有独立的数据,独立的堆栈空间.一个程序想要并发执行,开多个进程即可. Q1:在单核下,进程之间如何同时执行? 首先要区分两个概念--并发和并行 并发:并发是指在一段微小的时间段中,有多个程序代码段被CPU执行,宏观上表现出来就是多个程序能"同时"执行. 并行:并行是指在一个时间点,有多个程序段代码被CPU执行,它才是真正的同时执行. 所

  • 详解JAVA 函数式编程

    1.函数式接口 1.1概念: java中有且只有一个抽象方法的接口. 1.2格式: 修饰符 interface 接口名称 { public abstract 返回值类型 方法名称(可选参数信息); // 其他非抽象方法内容 } //或者 public interface MyFunctionalInterface { void myMethod(); } 1.3@FunctionalInterface注解: 与 @Override 注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解

  • 详解Python IO编程

    文件读写 读文件 try: # windows下utf8 f = open('./README.md', 'r', encoding='utf8', errors='ignore') print(f.read()) finally: f and f.close() # 用with简化 with open('./README.md', 'r', encoding='utf8') as f: print(f.read()) # 迭代读取大文件 with open('./README.md', 'r'

  • 详解python tcp编程

    网络连接与通信是我们学习任何编程语言都绕不过的知识点. Python 也不例外,本文就介绍因特网的核心协议 TCP ,以及如何用 Python 实现 TCP 的连接与通信. TCP 协议 TCP协议(Transmission Control Protocol, 传输控制协议)是一种面向连接的传输层通信协议,它能提供高可靠性通信,像 HTTP/HTTPS 等网络服务都采用 TCP 协议通讯.那么网络通讯方面都会涉及到 socket 编程,当然也包括 TCP 协议. Network Socket 我

  • 详解python UDP 编程

    前面我们讲了 TCP 编程,我们知道 TCP 可以建立可靠连接,并且通信双方都可以以流的形式发送数据.本文我们再来介绍另一个常用的协议–UDP.相对TCP,UDP则是面向无连接的协议. UDP 协议 我们来看 UDP 的定义: UDP 协议(User Datagram Protocol),中文名是用户数据报协议,是 OSI(Open System Interconnection,开放式系统互联) 参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务. 从这个定义中,我们可以总结

  • 详解JavaScript 异步编程

    异步的概念 异步(Asynchronous, async)是与同步(Synchronous, sync)相对的概念. 在我们学习的传统单线程编程中,程序的运行是同步的(同步不意味着所有步骤同时运行,而是指步骤在一个控制流序列中按顺序执行).而异步的概念则是不保证同步的概念,也就是说,一个异步过程的执行将不再与原有的序列有顺序关系. 简单来理解就是:同步按你的代码顺序执行,异步不按照代码顺序执行,异步的执行效果更高: 以上是关于异步的概念的解释,接下来我们通俗地解释一下异步:异步就是从主线程发射一

随机推荐