条款44:将与参数无关的代码抽离template
引起代码膨胀的种类:
●非类型模板参数(non-type template parameters)。文中做了举例,比如模板template <typename T, std::size_t n>,其中的n就是非类型模板参数,因为它是个常量嘛。这种参数会在编译器被展开。不同的参数会展开成不同的模板,导致代码膨胀。
●类型参数(type parameters)。这里主要说的是,在编译器中,它们是不同的类型,但实际上在二进制表达上,他们是相同的类型,但是链接器没有做去重。比如文中举例,有的机器上,int和long在二进制层面其实是一样的。这样的话vector和vector实际上编译完之后的二进制代码是完全一样的。这就造成了代码膨胀。
对于非类型模板参数,文中进行了详细的举例:
●写一个模板类,可以计算一个矩阵的逆。
// version:t44a.cc
#include <iostream>
#include <stdexcept>
template <typename T, std::size_t n>
class SquareMatrix {
public:
void invert() {
std::cout << "SquareMatrix invert:" << n << std::endl;
}
T data[n * n];
};
int main() {
SquareMatrix<double, 10> m1 = SquareMatrix<double, 10>();
m1.invert();
SquareMatrix<int, 5> m2 = SquareMatrix<int, 5>();
m2.invert();
return 0;
}
●这种写法,不同的n,比如SquareMatrix<double, 5>和SquareMatrix<double, 10>,会展开出不同的实现,导致代码膨胀。为了解决这个问题,可以通过传参的方式来获取n。不过,要我说,这么写不行吗?
// version:t44b.cc
#include <iostream>
#include <stdexcept>
template <typename T>
class SquareMatrix {
public:
SquareMatrix (std::size_t n) {
size = n;
data = new T[size * size];
if (data == nullptr) {
throw std::runtime_error("init memory error");
}
}
void invert() {
std::cout << "SquareMatrix invert:" << size << std::endl;
}
private:
T* data = nullptr;
std::size_t size = 0;
};
int main() {
SquareMatrix<double> m1 = SquareMatrix<double>(10);
m1.invert();
SquareMatrix<int> m2 = SquareMatrix<int>(5);
m2.invert();
return 0;
}
我觉得也没啥问题,是吧?除了new的时候失败不好处理异常,但大部分时间这里不会出问题。
不过书里是这么写的,把不带n的函数给抽出来一个基类。
// version:t44c.cc
#include <iostream>
#include <stdexcept>
template <typename T>
class SquareMatrixBase {
protected:
void invert(T* data, std::size_t n) {
std::cout << "SquareMatrixBase invert:" << n << std::endl;
}
};
template <typename T, std::size_t N>
class SquareMatrix : private SquareMatrixBase<T> {
private:
using SquareMatrixBase<T>::invert;
public:
void invert() {
std::cout << "SquareMatrix invert:" << N << std::endl;
this->invert(data, N);
}
T data[N * N];
};
int main() {
SquareMatrix<double, 10> m1 = SquareMatrix<double, 10>();
m1.invert();
SquareMatrix<int, 5> m2 = SquareMatrix<int, 5>();
m2.invert();
return 0;
}
- 注意:
- SquareMatrix继承的是SquareMatrixBase,也就是说,这两个对象:SquareMatrix<int, 5>和SquareMatrix<int, 10>,继承的是同一个基类SquareMatrix,这个基类的invert()在二进制里只存在一份,不会存在多份。
- SquareMatrixBase中的invert是protected,而SquareMatrix中的invert是public的。也就是说,SquareMatrix是真正暴露给用户的接口,而SquareMatrixBase只是一种辅助类,他们的继承不是is-a的关系。
- 继承类SquareMatrix调用基类SquareMatrixBase中的方法,是inline的。(待理解)
- this、using SquareMatrixBase::invert;见条款43。待理解
- 后面开始关注继承类SquareMatrix如何传递要操作的内存给SquareMatrixBase的问题。上一版代码是通过在基类的invert方法里加上T* data指针来传递的。这样的问题是,这个类理论上不止invert这一个方法,还有其他方法,每个方法都要加上一个参数,来告诉它要操作的数据地址,这不够简洁。所以,需要一种方法,可以让基类一次获取到地址,之后都操作这一块内存。
- 方法一:给基类加个指针成员变量,在构造时候初始化指向要操作的内存。真正的数据存放在子类里。
// version:t44d.cc
#include <iostream>
#include <stdexcept>
template <typename T>
class SquareMatrixBase {
protected:
SquareMatrixBase(T* d, std::size_t n) : data(d), size(n) {}
void invert() {
std::cout << "SquareMatrixBase invert, data:" << data << " size:" << size << std::endl;
}
private:
T* data;
std::size_t size;
};
template <typename T, std::size_t N>
class SquareMatrix : private SquareMatrixBase<T> {
public:
using SquareMatrixBase<T>::invert;
SquareMatrix() : SquareMatrixBase<T>(data, N) {}
void invert() {
std::cout << "SquareMatrix invert:" << N << std::endl;
SquareMatrixBase<T>::invert();
}
T data[N * N];
};
int main() {
SquareMatrix<double, 10> m1 = SquareMatrix<double, 10>();
m1.invert();
SquareMatrix<int, 5> m2 = SquareMatrix<int, 5>();
m2.invert();
return 0;
}
注意,子类调用基类invert时候,加上了SquareMatrixBase::以表明调用的是基类的invert,不然会出现递归。
- 方法二。直接把数据存放在对象里数据可能比较大,也不利于对象间传递,可以放到堆里。
//version:t44e.cc
#include <iostream>
#include <stdexcept>
#include <memory>
#include <vector>
template <typename T>
class SquareMatrixBase {
protected:
SquareMatrixBase() {}
void setPtr(std::vector<T>* d, std::size_t n) {
data.reset(d);
size = n;
}
void invert() {
std::cout << "SquareMatrixBase invert, data:" << data << " size:" << size << std::endl;
}
private:
std::shared_ptr<std::vector<T>> data;
std::size_t size;
};
template <typename T, std::size_t N>
class SquareMatrix : private SquareMatrixBase<T> {
public:
using SquareMatrixBase<T>::invert;
SquareMatrix() : SquareMatrixBase<T>() {
SquareMatrixBase<T>::setPtr(new std::vector<T>(N), N);
}
void invert() {
std::cout << "SquareMatrix invert:" << N << std::endl;
SquareMatrixBase<T>::invert();
}
T data[N * N];
};
int main() {
SquareMatrix<double, 10> m1 = SquareMatrix<double, 10>();
m1.invert();
SquareMatrix<int, 5> m2 = SquareMatrix<int, 5>();
m2.invert();
return 0;
}
- 迭代到此,解决了一系列问题。不过,还有一个问题是SquareMatrix<double, 5>和SquareMatrix<double, 10>,虽然都继承的SquareMatrixBase,但是互相之间是没法进行动态转换的。
比较
- t44a和后面的共享基类版本比
- 虽然代码膨胀了,但是可能产出更高效的代码。比如非类型参数可以在编译器替换成常量直接嵌入进代码里,提升执行效率。如果传参的话,就需要传递变量等。
- 共享基类版本,共享了基类的代码,能做到更好的代码局部性。
- 共享基类版本,每个类都带了一个基类,对象的大小会更大。