类的成员函数也可以是模板,无论这个类是个普通类,还是其本身也是个模板类。
template<typename T>
class Stack{
T elems[100];
};
Stack<int> s1, s2;
Stack<double> s3;
s2 = s1; // OK, same type
s3 = s1; // Error, different types
这里我们假设数组之间可以直接赋值操作,因为这个不是我们讲述的重点。由不同类型实例化得到的Stack的类型是不一样的,Stack<int>和Stack<double>是两个类型,而编译器缺省生成的operator=只能用于相同类型的对象之间的赋值。那就添加一个定义,因为=右边Stack实例化的类型不确定,所以它应该是个模板函数。
template<typename T>
class Stack {
T elems[100];
public:
template<typename T2>
Stack& operator=(const Stack<T2>&);
};
template<typename T>
template<typename T2>
Stack<T>& Stack<T>::operator=(const Stack<T2>& rhs) {
for (int i = 0; i < 100; ++i) {
elems[i] = rhs.elems[i];
}
return *this;
}
这里特地将operator=的实现放在了类定义的外面。这里需要两个template,它们分别引出类Stack和函数operator=的模板参数列表。
很遗憾,编译失败。elems是私有成员,你无权访问。怎么办?老办法,让它成为友元。
template<typename T>
class Stack {
T elems[100];
public:
template<typename T2>
Stack& operator=(const Stack<T2>&);
template<typename>
friend class Stack;
};
这次一切OK。
下面看看类成员函数模板的特化。因为函数不支持部分特化,所以只能是类模板和成员函数模板同时特化。成员函数模板的特化不需要声明,只需要直接给出特化定义即可。
template<>
template<>
inline Stack<int>& Stack<int>::operator=<std::string>(const Stack<std::string>& rhs) {
for (int i = 0; i < 100; ++i) {
elems[i] = rhs.elems[i].size();
}
return *this;
}
我们特化了Stack<int>时的=(Stack<std::string>)这一特殊场合。特化定义其实是个普通函数,虽然带着两个template<>,看着有点怪怪的。如果定义在头文件中,别忘记了加上inline。
大家都知道类里面有几个特殊的成员函数:拷贝构造、移动构造、拷贝赋值、移动赋值。它们对类的使用是如此的重要,以至于如果你不定义它们,编译器会按照缺省语义替你生成。你同样可以定义这些成员函数的模板版本(上面的例子就是定义了赋值操作符的模板版本。不过有一点要记住,模板版本不会代替缺省版本。如上面的例子,定义的模板版本赋值操作符仅用于不同类型的Stack之间的赋值,如果是相同的类型,仍然会调用那个编译器生成的缺省版本)。
模板版本和缺省版本的匹配竞争结果有时候会让你大吃一惊,模板版本会在意料之外越俎代庖优先匹配。
class Person {
std::string name;
public:
template<typename T>
explicit Person(T&& s) : name(std::forward<T>(s)) {
std::cout << "Person(T&& s)\n";
}
Person(const Person& p) : name(p.name) { std::cout << "Person(const Person& p)\n"; }
};
我们定义了一个模板构造函数,利用转发语义同时处理调用参数是左值和右值的情形,完美高效(前提是只要参数类型能够用来完成std::string的构造)。
std::string s = "first name";
Person p1(s); // 左值引用
Person p2("second name"); // 右值引用,name直接从临时对象构建,节省了一次构造和析构
Person p3(p2); // 编译错误:no matching constructor for initialization of 'std::string'
没有匹配的构造函数?!可是我们明明定义了拷贝构造函数了,编译器居然看不到吗?编译器看到了,但却没有选择它,原因是这样的:p2的类型是Person,而我们定义的拷贝构造函数的参数类型是const Person&,这里就存在一个转换,由非const转换为const,这个通常没有问题。但现在多出了一个候选人,那个模板构造函数,它的匹配度更高,它利用转发语义的类型推导得到的参数类型可以精确匹配p2的类型,它不需要这个转换,结果它赢了。可是p2这个Person去初始化name时却无法成功,结果你得到了编译错误。
既然是这样,那我再加一个非const的构造函数,不就可以了吗?
Person(Person& p) : name(p.name) { std::cout << "Person(Person& p)\n"; }
这样确实可以,而且调用也匹配了这个新的构造函数。但是我们没有解决这个问题的核心(头疼医头不是我们程序员的风格),这个问题的本质是当用一个Person对象来构造Person时,我们需要的是编译器去选用拷贝构造函数,那个模板构造函数只是用于其他类型的参数(只要能初始化std::string)的。也就是说在某些场合我们要禁用这个模板构造函数,让真正的五件套上场发挥作用。
class Person {
std::string name;
public:
template<typename T, typename = std::enable_if_t<!std::is_same_v<Person, std::decay_t<T>>>>
explicit Person(T&& s) : name(std::forward<T>(s)) {
std::cout << "Person(T&& s)\n";
}
Person(const Person& p) : name(p.name) { std::cout << "Person(const Person& p)\n"; }
};
这次给模板构造函数增加了第二个模板参数,其实它只是一个约束,当约束不成立时,这个模板的定义就是无效的,这个时候就会自动匹配其他的构造函数了。这个限定很简单,在T推导出来的类型是Person类型时禁用这个模板。这个才是我们真正想要的!
好了,编译、运行,一切都如偿所愿。