条款44:将与参数无关的代码抽离template

条款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和后面的共享基类版本比
    • 虽然代码膨胀了,但是可能产出更高效的代码。比如非类型参数可以在编译器替换成常量直接嵌入进代码里,提升执行效率。如果传参的话,就需要传递变量等。
    • 共享基类版本,共享了基类的代码,能做到更好的代码局部性。
    • 共享基类版本,每个类都带了一个基类,对象的大小会更大。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值