函数模板可以实现对于不同的类型复用同一套代码,但是某些情况下,某些类型需要专门去实现,不能套用模板,举例比如对于print函数,可以打印整型、浮点型甚至字符,但是如果输入一个指针呢?所以这个时候就需要对指针类型的这个函数进行专门定制。
假设是下面这个类:
template <typename T>
class Storage{
private: T m_value;
public:
Storage(T value){m_value = value;}
~Storage(){}
void print(){
std::cout << m_value<<'\n';
}
}
这个模板类对很多类型都适用,比如:
Storage<int> nValue(5);
Storage<double> dValue(6.7);
nValue.print();
dValue.print();
假如我们没有进行模板特化,传入一个指针类型会发生什么?如下面代码所示
const char *string = new char[40];
string = "abcde";
Storage<const char*> storage(string);
delete[] string;
string.print();
如果中间没有那一句delete[] string
则程序会正常输出,但是上面的代码却在vs里面直接报错。因为这里面在类初始化中,构造函数如下:
template <>
Storage<char*>::Storage(char* value)
{
m_value = value;
}
这里面只做了一个浅拷贝,当把外部的变量删了之后,这个自然就找不到位置了。所以需要针对某些特定类型做模板的定制 。
具体规则就是在要具体定义的函数前面加上
template <>
。因为这里就是具体规定类型,所以就用空<>代替即可。
template <>
Storage<char*>::Storage(char* value)
{
// Figure out how long the string in value is
int length=0;
while (value[length] != '\0')
++length;
++length; // +1 to account for null terminator
// Allocate memory to hold the value string
m_value = new char[length];
// Copy the actual value string into the m_value memory we just allocated
for (int count=0; count < length; ++count)
m_value[count] = value[count];
}
具体的函数专业化其实是已经结束了,但是作为代码的完整性,还不够,因为上面内部开辟了一块内存空间,但是,没有释放这一块内存。所以完整代码还需要补充一块析构函数。
template <>
Storage<char*>::~Storage()
{
delete[] m_value;
}
大体思路和函数模板定制接近,教程选了一个比较难的例子。
template <class T>
class Storage8
{
private:
T m_array[8];
public:
void set(int index, const T &value)
{
m_array[index] = value;
}
const T& get(int index) const
{
return m_array[index];
}
};
对于上面的例子,初始化类为Storage8<int> intStorage
什么的都可以,但是如果初始化为一个Storage8<bool> boolStorage
,一个变量都至少需要一个字节,所以本来只需要一个字节来存储的,但是目前需要8个字节,所以对于类型为bool
的类,为了提高空间利用效率,可以针对这个类型专门定制。
那为什么不重新定义一个类呢?因为我们必须给它起一个不同的名字。然后,程序员必须记住,Storage8 是用于非布尔类型的,而Storage8Bool(或我们称之为新类的任何名称)是布尔的。我们宁愿避免这种不必要的复杂性。
规则:
首先,请注意,我们从开始template<>。template关键字告诉编译器以下内容已模板化,而空括号则表示没有任何模板参数。在这种情况下,没有任何模板参数,因为我们将唯一的模板参数(T)替换为特定类型(bool)。
接下来,我们在类名称中添加以表示我们正在对Storage8类的bool版本进行专用化。
template <> // the following is a template class with no templated parameters
class Storage8<bool> // we're specializing Storage8 for bool
{
// What follows is just standard class implementation details
private:
unsigned char m_data{};
public:
void set(int index, bool value)
{
// Figure out which bit we're setting/unsetting
// This will put a 1 in the bit we're interested in turning on/off
auto mask{ 1 << index };
if (value) // If we're setting a bit
m_data |= mask; // Use bitwise-or to turn that bit on
else // if we're turning a bit off
m_data &= ~mask; // bitwise-and the inverse mask to turn that bit off
}
bool get(int index)
{
// Figure out which bit we're getting
auto mask{ 1 << index };
// bitwise-and to get the value of the bit we're interested in
// Then implicit cast to boolean
return (m_data & mask);
}
};
上面两部分的专业化其实是模板的完全定制,就是所有的参数都确定下后进行定义,但是对于含有非类型参数的类型:
template <class T, int size> // size is the expression parameter
class StaticArray{......}
对于这种模板,可能有时候需要定制的是第一个参数,第二个参数个数不能被定制。这种定制被称为部分模板定制化。
举例一个打印上述类内容的函数:
template <typename T, int size>
void print(StaticArray<T, size> &array)
{
for (int count{ 0 }; count < size; ++count)
std::cout << array[count] << ' ';
}
假设我们需要对StaticArray
类中char
类型进行print
函数定制的话?怎么写呢?首先因为typename T
已经被固定了,所以首行中的template
中不需要在进行类型定义了,只需要保留第二个参数,定义即:
template <typename T, int size>
void print(StaticArray<T, size> &array)
{
for (int count{ 0 }; count < size; ++count)
std::cout << array[count] << ' ';
}
// overload of print() function for partially specialized StaticArray<char, size>
template <int size> //不能定制的参数,这里要保留
void print(StaticArray<char, size> &array) //注意这里面Staticarray中的类型要明确
{
for (int count{ 0 }; count < size; ++count)
std::cout << array[count];
}
如果我们要对一个类中的某些函数进行部分定制怎么办呢?
如下面这个类中的print
函数。
template <class T, int size> // size is the expression parameter
class StaticArray
{
private: T m_array[size]{};
public:
T* getArray() { return m_array; }
T& operator[](int index){ return m_array[index];}
void print(){
for (int i{ 0 }; i < size; ++i)
std::cout << m_array[i] << ' ';
std::cout << '\n';
}
};
最直观的想法就是这样写:
// Error!!!!
template <int size>
void StaticArray<double, size>::print()
{
for (int i{ 0 }; i < size; ++i)
std::cout << std::scientific << m_array[i] << ' ';
std::cout << '\n';
}
然而这样写是错误的。其实也可以直接重新定义这个类:
template <int size>
void StaticArray<double, size>
{......}
但是由于我们只需要重写print
函数,所以这样写就会有很多重复的代码。
解决这个问题其实没有进一步的语法可以规避,靠的是一个trick
。
我们可以定义一个基础类:
template <class T, int size> // size is the expression parameter
class StaticArray_Base
{......}
然后一般的类都这样用:
template <class T, int size> // size is the expression parameter
class StaticArray: public StaticArray_Base<T, size>
{
public:
};
对于要重写的类,可以这么写:
template <int size> // size is the expression parameter
class StaticArray<double, size>: public StaticArray_Base<double, size>
{
public:
void print()
{
for (int i{ 0 }; i < size; ++i)
std::cout << std::scientific << this->m_array[i] << ' ';
// note: The this-> prefix in the above line is needed.
// See https://stackoverflow.com/a/6592617 or https://isocpp.org/wiki/faq/templates#nondependent-name-lookup-members for more info on why.
std::cout << '\n';
}
};
重温一下前面使用过的一个例子:
template <class T>
class Storage
{
private:
T m_value;
public:
Storage(T value)
{
m_value = value;
}
~Storage()
{
}
void print()
{
std::cout << m_value << '\n';
}
};
前面的例子中,我们定制化了char*
这个类型。但是问题其实还有:如果是int*
float*
……这些指针怎么办呢?
int*
float*
这些指针明显又有很多相通的地方,所以指针很多类型其实又可以规划为一大类,进行指针类型定制。如下:
template <typename T>
class Storage<T*> // this is a partial-specialization of Storage that works with pointer types
{
private:
T* m_value;
public:
Storage(T* value) // for pointer type T
{
// For pointers, we'll do a deep copy
m_value = new T(*value); // this copies a single value, not an array
}
~Storage()
{
delete m_value; // so we use scalar delete here, not array delete
}
void print()
{
std::cout << *m_value << '\n';
}
};
值得注意的是,由于此部分专用的Storage
类仅分配一个值,因此对于C样式的字符串,将仅复制第一个字符。如果希望复制整个字符串,则可以完全专门化char *
类型的构造函数(和析构函数)。完全专业的版本将优先于部分专业的版本。这是一个示例程序,它既使用指针的部分专门化,又使用char *
的完全专门化:
完全体代码:
#include <iostream>
#include <cstring>
// Our Storage class for non-pointers
template <class T>
class Storage
{
private:
T m_value;
public:
Storage(T value)
{
m_value = value;
}
~Storage()
{
}
void print()
{
std::cout << m_value << '\n';
}
};
// Partial-specialization of Storage class for pointers
template <class T>
class Storage<T*>
{
private:
T* m_value;
public:
Storage(T* value)
{
m_value = new T(*value);
}
~Storage()
{
delete m_value;
}
void print()
{
std::cout << *m_value << '\n';
}
};
// Full specialization of constructor for type char*
template <>
Storage<char*>::Storage(char* value)
{
// Figure out how long the string in value is
int length = 0;
while (value[length] != '\0')
++length;
++length; // +1 to account for null terminator
// Allocate memory to hold the value string
m_value = new char[length];
// Copy the actual value string into the m_value memory we just allocated
for (int count = 0; count < length; ++count)
m_value[count] = value[count];
}
// Full specialization of destructor for type char*
template<>
Storage<char*>::~Storage()
{
delete[] m_value;
}
// Full specialization of print function for type char*
// Without this, printing a Storage<char*> would call Storage<T*>::print(), which only prints the first element
template<>
void Storage<char*>::print()
{
std::cout << m_value;
}
int main()
{
// Declare a non-pointer Storage to show it works
Storage<int> myint(5);
myint.print();
// Declare a pointer Storage to show it works
int x = 7;
Storage<int*> myintptr(&x);
// If myintptr did a pointer assignment on x,
// then changing x will change myintptr too
x = 9;
myintptr.print();
// Dynamically allocate a temporary string
char *name = new char[40]{ "Alex" }; // requires C++14
// If your compiler isn't C++14 compatible, comment out the above line and uncomment these
// char *name = new char[40];
// strcpy(name, "Alex");
// Store the name
Storage< char*> myname(name);
// Delete the temporary string
delete[] name;
// Print out our name
myname.print();
}