Effective C++ 筆記 —— Item 44: Factor parameter-independent code out of templates.
For example, suppose you'd like to write a template for fixed-size square matrices that, among other things, support matrix inversion.
template<typename T, std::size_t n> // template for n x n matrices of objects of type T; see below for info on the size_t parameter class SquareMatrix { public: // ... void invert(); // invert the matrix in place }; SquareMatrix<double, 5> sm1; // ... sm1.invert(); // call SquareMatrix<double, 5>::invert SquareMatrix<double, 10> sm2; // ... sm2.invert(); // call SquareMatrix<double, 10>::invert
Two copies of invert will be instantiated here. The functions won't be identical, because one will work on 5 ×5 matrices and one will work on 10 ×10 matrices, but other than the constants 5 and 10, the two functions will be the same. This is a classic way for template-induced code bloat to arise.
Here's a first pass at doing that for SquareMatrix:
template<typename T> // size-independent base class for square matrices class SquareMatrixBase { protected: //... void invert(std::size_t matrixSize); // invert matrix of the given size //... }; template<typename T, std::size_t n> classSquareMatrix : private SquareMatrixBase<T> { private: using SquareMatrixBase<T>::invert; // make base class version of invert visible in this class; see Items 33 and 43 public: //... void invert() { invert(n); } // make inline call to base class };
All matrices holding a given type of object will share a single SquareMatrixBase class. They will thus share a single copy of that class's version of invert.
- SquareMatrixBase::invert is intended only to be a way for derived classes to avoid code replication, so it's protected instead of being public.
- The additional cost of calling it should be zero, because derived classes' inverts call the base class version using inline functions. (The inline is implicit — see Item 30.)
- Notice also that the inheritance between SquareMatrix and SquareMatrixBase is private. This accurately reflects the fact that the reason for the base class is only to facilitate thederived classes' implementations, not to express a conceptual is-a relationship between SquareMatrix and SquareMatrixBase. (For information on private inheritance, see Item 39)
So far, so good, but there's a sticky issue we haven't addressed yet. How does SquareMatrixBase::invert know what data to operate on?
Have SquareMatrixBase store a pointer to the memory for the matrix values. And as long as it's storing that, it might as well store the matrix size, too. The resulting design looks like this:
template<typename T> class SquareMatrixBase { protected: SquareMatrixBase(std::size_t n, T *pMem) // store matrix size and a ptr to matrix values : size(n), pData(pMem) {} void setDataPtr(T *ptr) { pData = ptr; } // reassign pData //... private: std::size_t size; // size of matrix T *pData; // pointer to matrix values };
This lets derived classes decide how to allocate the memory. Some implementations might decide to store the matrix data right inside the SquareMatrix object:
template<typename T, std::size_t n> class SquareMatrix : private SquareMatrixBase<T> { public: SquareMatrix() // send matrix size and : SquareMatrixBase<T>(n, data) {} // data ptr to base class //... private: T data[n*n]; };
Objects of such types have no need for dynamic memory allocation, but the objects themselves could be very large. An alternative would be to put the data for each matrix on the heap
template<typename T, std::size_t n> class SquareMatrix : private SquareMatrixBase<T> { public: SquareMatrix() // set base class data ptr to null, : SquareMatrixBase<T>(n, 0), // allocate memory for matrix values, save a ptr to the memory, and give a copy of it to the base class pData(new T[n*n]) // { this->setDataPtr(pData.get()); } // ... private: boost::scoped_array<T> pData; // see Item 13 for info on boost::scoped_array };
This Item has discussed only bloat due to non-type template parameters, but type parameters can lead to bloat, too. For example, on many platforms, int and long have the same binary representation, so the member functions for, say, vector and vector would likely be identical — the very definition of bloat. Some linkers will merge identical function implementations, but some will not, and that means that some templates instantiated on both int and long could cause code bloat in some environments. Similarly, on most platforms, all pointer types have the same binary representation, so templates holding pointer types (e.g., list<int*>, list, list<squarematrix<long, 3="">*>, etc.) should often be able to use a single underlying implementation for each member function. Typically, this means implementing member functions that work with strongly typed pointers (i.e., T* pointers) by having them call functions that work with untyped pointers (i.e., void* pointers). Some implementations of the standard C++ library do this for templates like vector, deque, and list. If you're concerned about code bloat arising in your templates, you'll probably want to develop templates that do the same thing.
Things to Remember
- Templates generate multiple classes and multiple functions, so any template code not dependent on a template parameter causes bloat.
- Bloat due to non-type template parameters can often be eliminated by replacing template parameters with function parameters or class data members.
- Bloat due to type parameters can be reduced by sharing implementations for instantiation types with identical binary representations