0. 何为模板特化
C++模板最普遍的用法是为一个(编译期确定的)未知类型定义类或者函数。但有时会遇到这样一种情况:这些通用的实现并不满足某些特定类型下的需求。这时,就需要使用C++模板特化,为指定类型定义一个不同的实现。
C++的模板特化有两种:全特化(可用于类或函数)以及偏特化(仅能用于类)。
全特化意味着,该实现的所有模板参数都已经被确定,对应的,偏特化表示其中只有部分模板参数是确定的。本篇将只介绍全特化,后者使用场景不多,等下一篇再展开介绍。
2. 全特化 in action
2.1. 定义一个使用场景
在工程中比较底层的代码中,经常会遇到需要做特殊处理的模板类型。例如,定义一个通用的to_str<T>()
函数,或者定义一个lesser_than<T>(a, b)
,不同类型有着不同的处理方式。
本篇以实现通用的to_str<T>(v)
函数为例,简单介绍全特化的使用方法。
2.2. 定义一个通用的实现
首先我们定义一个通用的函数如下:
// to_str.h
template<typename T>
std::string to_str(const T& t)
{
std::ostringstream oss;
oss << "trival value: [" << t << "]";
return oss.str();
}
内容很简单,无非是把typename T
类型使用std::ostringstream
进行导出处理(关于<<
操作符在后面会有解释,见第3节)。
现在,我们可以在任何地方,只要引用了这个文件,就可以做一些简单的转字符串操作了:
std::cout << "Trival Values:" << std::endl;
std::cout << to_str(32) << std::endl;
std::cout << to_str("const char *") << std::endl;
std::cout << to_str(34.56) << std::endl;
2.3 声明并定义一个全特化实现
但假如我们有这样一个类,很明显是无法直接使用这个函数进行to_str()
操作的:
// my_special_class.h
class MySpecialClass
{
public:
int val1;
int val2;
explicit MySpecialClass(int val1, int val2) : val1(val1), val2(val2)
{
}
};
2.3.1. 普通实现
首先我们声明一个定义如下:
// to_str.h
template<>
std::string to_str<MySpecialClass>(const MySpecialClass& t);
有三点可能会引起注意的地方:
template<>
标明这是一个模板相关的实现;- 这里只进行了声明,但并没有做任何定义;
- 模板特化声明要放在通用类型的声明与定义之后,否则编译器不认识这个声明。
这时,我们将在头文件以外实现它的定义,此处放到to_str.cpp
中:
// to_str.cpp
#include "to_str.h"
#include "my_special_class.h"
template<>
std::string to_str<MySpecialClass>(const MySpecialClass& t)
{
return t.str();
}
这样我们就可以如下使用to_str()
函数了:
MySpecialClass spclass{
12, 34
};
std::cout << to_str(spclass) << std::endl;
2.3.2. inline实现
普通实现中,可以注意到,它像一个普通的函数一样,将声明与定义分拆到了.h
头文件和.cpp
源文件中。为什么不直接将如下定义像模板通用实现一样放到.h
头文件中呢?
// to_str.cpp
template<>
std::string to_str<MySpecialClass>(const MySpecialClass& t)
{
return t.str();
}
原因是,已经全特化的函数就等于一个普通的函数,在头文件中进行定义会遇到那个经典的“重定义”问题。解决方法也很简单,要么如前一小节中所述,拆分声明与定义,要么在函数声明中加入inline
关键字,这也表示着“使用该定义做展开”:
// to_str.h
template<>
inline std::string to_str<MySpecialClass>(const MySpecialClass& t)
{
return t.str();
}
3. 对<<
操作符的一点解释
在本示例中,还有一种方法,可以不使用全特化,但仍然可以用to_str
模板函数,方法就是为MySpecialClass
定义<<
操作符。
这里暂时不展开太多内容,将来可能会另外记录一下各种操作符的使用经验。
4. 完整示例代码
// to_str.h
#pragma once
#ifndef TO_STR_H_
#define TO_STR_H_
#include <string>
#include <sstream>
class MySpecialClass;
template<typename T>
std::string to_str(const T& t)
{
std::ostringstream oss;
oss << "trival value: [" << t << "]";
return oss.str();
}
template<>
std::string to_str<MySpecialClass>(const MySpecialClass& t);
template<>
std::string to_str<std::string>(const std::string& t)
{
return "[" + t + "]";
}
#endif // !TO_STR_H_
// my_special_class.h
#pragma once
#ifndef MY_SPECIAL_CLASS_H_
#define MY_SPECIAL_CLASS_H_
#include <string>
#include <sstream>
class MySpecialClass
{
public:
int val1;
int val2;
explicit MySpecialClass(int val1, int val2) : val1(val1), val2(val2)
{
}
std::string str() const
{
std::ostringstream oss;
oss << "val1: [" << val1 << "], val2: [" << val2 <<"]";
return oss.str();
}
};
#endif // !MY_SPECIAL_CLASS_H_
// to_str.cpp
#include "to_str.h"
#include "my_special_class.h"
template<>
std::string to_str<MySpecialClass>(const MySpecialClass& t)
{
return t.str();
}
// main.cpp
#include <iostream>
#include "to_str.h"
#include "my_special_class.h"
int main(int argc, char** argv)
{
MySpecialClass spclass{
12, 34
};
std::cout << "Trival Values:" << std::endl;
std::cout << to_str(32) << std::endl;
std::cout << to_str("const char *") << std::endl;
std::cout << to_str(34.56) << std::endl;
std::cout << std::endl;
std::cout << "Template Specialization:" << std::endl;
std::cout << to_str(std::string("string val")) << std::endl;
std::cout << to_str(spclass) << std::endl;
return 0;
}
结果如下: