Effective_C++_条款四十四:将与参数无关的代码抽离template

2024-01-20 20:38

本文主要是介绍Effective_C++_条款四十四:将与参数无关的代码抽离template,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

标题上说“将与参数无关的代码抽离template”,这里的参数既可以指类型,也可以是非类型,我们先来看看非类型的情况。

假定我们要为矩阵写一个类,这个矩阵的行列元素个数相等,是一个方阵,因而我们可以对之求逆运算。因为方阵的元素可以有多种类型,同时方阵的维数(方阵大小)也可以不同,像下面这样,我们使用了模板:

template <class T, size_t n>
class SquareMatrix
{
public:void Invert();
};int main()
{SquareMatrix<int, 10> a;SquareMatrix<int, 5> b;
}

模板既可以指定类型,也可以指定其他参量,比如这里的size_t,用来表示方阵的维数。程序看起来是没有问题的,编译也是通过的,但从代码优化上看,还是有空间可以做的。a和b虽然都是元素为int型的方阵,但因为方阵维数不同,因而生成了两个Invert函数,这两个Invert函数的代码只是在循环个数上不同(取觉于方阵维数n),但算法思想完全是一样的。

为了防止编译器生成冗余的代码,我们可以将Invert抽离这里,变成一个以n为参数的函数,像下面这样:

class SquareMatrixBase
{
public:SquareMatrixBase(T* p) : DataPointer(p){}void Invert(size_t n){}
private:T* DataPointer;
};template <class T, size_t n>
class SquareMatrix: private SquareMatrixBase<T>//private 继承
{
public:SquareMatrix() : SquareMatrixBase(Data){}void Invert(){SquareMatrixBase::Invert(n);//调父类函数}
private:T Data[n * n];
};

这里我们定义了一个BaseSquareMatrix类,这个类可以视为一个工具类,因为SquareMatrix是private继承于它的,我们把算法一致、只与维数n相关的算法函数都放在BaseSquareMatrix中,且将n作为它的Invert()函数的形参,为了使BaseSquareMatrix可以访问数据,从而求逆运算,我们声明了它的成员指针,这个指针将会在构造时指向子类的数据存储空间。

子类SquareMatrix没有大变化,只是在Invert的时候,用了一句代码就搞定了——调用父类的Invert函数,并把维数n传过去。

这样在main函数中,针对类型都是int,但矩阵维数不同的情况,Invert中生成的冗余代码非常少(只有一句话),父类也因为模板参数与维数无关,所以也只生成一份。从这个角度来看,将与参数无关的代码抽离template起到了减少代码冗余度的优化作用。(???但是在子类中依然有size 模板参数)

 

目前只是说将与类型无关的代码进行抽离(造一个父类,把算法相同的部分提取出来,放到父类),对于不同类型,其实也可以通过void*来实。比如STL,要想令我们使用list<int*>,list<const int*>生成的代码冗余度保持很低,这就需要在相应的函数里面,将之链到父类的一个公共实现版本里面,这个版本的形参是void*。

 

最后总结一下:

1. Template生成多个classes与多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。

2. 因非类型模板参数而造成的代码膨胀,往往可以消除,做法是以函数参数或者class成员变量替换template参数。

3. 因类型而造成的代码膨胀,也可以降低,做法是让带有完全相同二进制表述的具现类型共享实现码。

 

  • 模板是个好东西,你可以在保证类型安全的同时用一份代码同时兼容不同类型。 但模板提供的是编译期的多态, 即使你的代码看起来非常简洁短小,生成的二进制文件也可能包含大量的冗余代码。 因为模板每次实例化都会生成一个完整的副本,所以其中与模板参数无关的部分会造成代码膨胀(code bloat)。

把模板中参数无关的代码重构到模板外便可以有效地控制模板产生的代码膨胀。 另外代码膨胀也可以由类型模板参数产生:

  • 对于非类型模板参数产生的代码膨胀,用函数参数或数据成员来代替模板参数即可消除冗余;
  • 对于类型模板参数产生的代码膨胀,可以让不同实例化的模板类共用同样的二进制表示。

抽取公共代码

在避免代码冗余的问题上,抽取公共代码(commonality and variability analysis)是我们每天都在用的方法。 当你写几个函数时,会把其中的公共部分抽取到另一个函数;当你声明类时,也会把它们的公共部分抽取到父类中。

于是你希望在模板编程中也用该办法来避免代码重复,但模板和非模板代码在这一点上是不同的:

  • 非模板的代码中,冗余的显式的(explicit)。只要有重复代码你都会看到它;
  • 模板代码中,冗余是隐式的(implicit)。模板代码只有一份,模板被实例化时产生的冗余需要你的直觉才能感受到。

模板产生的代码膨胀

现在来看一个模板是怎样引发代码膨胀的。比如要实现一个固定大小的矩阵,它支持转置运算。

template<typename T, int n>
class Square{
public:void invert();
};

其中的int n是一个非类型参数,它也是一种合法的模板参数~

然后可能会这样使用该模板:

Square<double, 5> s1;
Square<double, 10> s2;
s1.invert();    s2.invert();

Square模板会实例化两个类:Square<double, 5>Square<double, 10>,它们拥有相同的invert方法。 这是模板产生代码膨胀的典型场景。

抽取父类模板

结局模板产生的代码膨胀,仍然是用抽取公共代码的办法。如果你真的看到了二进制代码中两个相同的invert函数, 你的直觉肯定是把它抽取到另一个类中:

template<typename T>
class SquareBase{
protected:void invert(int size);
};template<typename T, int n>
class Square:private SquareBase<T>{
private:using SquareBase<T>::invert;
public:void invert(){ this->invert(n); }
}

因为invert函数定义在基类中,所以它只会在二进制代码中出现一次,即SquareBase<double>::invert。该函数由两个子类共享。 上述代码中有些细节还值得一提:

  • SquareBase::invert是供子类用的,所以声明为private而不是public
  • 调用父类invert的代价为零,因为Square::invert是隐式的inline函数,见Item 30;
  • 使用this->前缀是因为,SquareBase里的名称在子类模板Square里是隐藏的,见Item 43;
  • 使用private继承是因为,Square is implemented in terms of Square,见Item 39。

数据存储问题

既然我们决定由父类来做invert操作,那么父类怎么访问数据呢?因为数据本来是在子类中的。 当然我们可以在调用SquareBase::invert时把内存地址也一起告知父类, 但如果矩阵类中有很多函数都需要这些信息呢?我们可能需要调用每个函数时都把这些信息传递给父类函数。 既然这样,何不把数据地址直接放在父类中?既然父类存放了数据,那么就把矩阵大小也一并存放了吧!

template<typename T>
class SquareBase{
protected:SquareBase(int _n, T *_data): n(_n), data(_data){}void setData(T *_data){data = _data;}
private:int n;T* data;
};

父类中存储了矩阵数据的位置(data)以及大小(n),子类仍然可以决定如何分配地址空间。 可以存放在子类中作为成员属性,也可以动态申请内存。

权衡

不管数据是怎样分配和访问的,我们消除代码重复的方案是确定的:将公共部分抽取到父模板类中。 这样做的好处便是避免了代码膨胀,减小了二进制文件和”working set”的大小,有利于提高指令缓存的命中率, 从而达到更高的代码执行效率。但提取公共部分到新的模板类也造成了一些问题:

  • 如果int n硬编码在模板参数中的话,编译器能做更多的优化,比如常量传播等。但int n作为函数参数,这些优化就没有了。
  • 添加类的层级会导致对象大小的增加。至少多存储了一个T* data指针。

实践中到底是否应该抽取公共代码出来取决于你的应用场景,在上述的优劣中进行权衡。

本问讨论的是非类型模板参数,对于类型模板参数,代码膨胀的问题也是存在的,比如

  • intlong在多数平台都是一样的底层实现,然而模板却会实例化为两份,因为它们类型不同。
  • List<int *>List<const int *>List<double *>的底层实现也是一样的。但因为指针类型不同,也会实例化为多份模板类。

 

这篇关于Effective_C++_条款四十四:将与参数无关的代码抽离template的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/627269

相关文章

Windows下C++使用SQLitede的操作过程

《Windows下C++使用SQLitede的操作过程》本文介绍了Windows下C++使用SQLite的安装配置、CppSQLite库封装优势、核心功能(如数据库连接、事务管理)、跨平台支持及性能优... 目录Windows下C++使用SQLite1、安装2、代码示例CppSQLite:C++轻松操作SQ

C++中RAII资源获取即初始化

《C++中RAII资源获取即初始化》RAII通过构造/析构自动管理资源生命周期,确保安全释放,本文就来介绍一下C++中的RAII技术及其应用,具有一定的参考价值,感兴趣的可以了解一下... 目录一、核心原理与机制二、标准库中的RAII实现三、自定义RAII类设计原则四、常见应用场景1. 内存管理2. 文件操

C++中零拷贝的多种实现方式

《C++中零拷贝的多种实现方式》本文主要介绍了C++中零拷贝的实现示例,旨在在减少数据在内存中的不必要复制,从而提高程序性能、降低内存使用并减少CPU消耗,零拷贝技术通过多种方式实现,下面就来了解一下... 目录一、C++中零拷贝技术的核心概念二、std::string_view 简介三、std::stri

C++高效内存池实现减少动态分配开销的解决方案

《C++高效内存池实现减少动态分配开销的解决方案》C++动态内存分配存在系统调用开销、碎片化和锁竞争等性能问题,内存池通过预分配、分块管理和缓存复用解决这些问题,下面就来了解一下... 目录一、C++内存分配的性能挑战二、内存池技术的核心原理三、主流内存池实现:TCMalloc与Jemalloc1. TCM

Python实例题之pygame开发打飞机游戏实例代码

《Python实例题之pygame开发打飞机游戏实例代码》对于python的学习者,能够写出一个飞机大战的程序代码,是不是感觉到非常的开心,:本文主要介绍Python实例题之pygame开发打飞机... 目录题目pygame-aircraft-game使用 Pygame 开发的打飞机游戏脚本代码解释初始化部

C++ 函数 strftime 和时间格式示例详解

《C++函数strftime和时间格式示例详解》strftime是C/C++标准库中用于格式化日期和时间的函数,定义在ctime头文件中,它将tm结构体中的时间信息转换为指定格式的字符串,是处理... 目录C++ 函数 strftipythonme 详解一、函数原型二、功能描述三、格式字符串说明四、返回值五

Java中Map.Entry()含义及方法使用代码

《Java中Map.Entry()含义及方法使用代码》:本文主要介绍Java中Map.Entry()含义及方法使用的相关资料,Map.Entry是Java中Map的静态内部接口,用于表示键值对,其... 目录前言 Map.Entry作用核心方法常见使用场景1. 遍历 Map 的所有键值对2. 直接修改 Ma

C++作用域和标识符查找规则详解

《C++作用域和标识符查找规则详解》在C++中,作用域(Scope)和标识符查找(IdentifierLookup)是理解代码行为的重要概念,本文将详细介绍这些规则,并通过实例来说明它们的工作原理,需... 目录作用域标识符查找规则1. 普通查找(Ordinary Lookup)2. 限定查找(Qualif

深入解析 Java Future 类及代码示例

《深入解析JavaFuture类及代码示例》JavaFuture是java.util.concurrent包中用于表示异步计算结果的核心接口,下面给大家介绍JavaFuture类及实例代码,感兴... 目录一、Future 类概述二、核心工作机制代码示例执行流程2. 状态机模型3. 核心方法解析行为总结:三

python获取cmd环境变量值的实现代码

《python获取cmd环境变量值的实现代码》:本文主要介绍在Python中获取命令行(cmd)环境变量的值,可以使用标准库中的os模块,需要的朋友可以参考下... 前言全局说明在执行py过程中,总要使用到系统环境变量一、说明1.1 环境:Windows 11 家庭版 24H2 26100.4061