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);

有三点可能会引起注意的地方:

  1. template<>标明这是一个模板相关的实现;
  2. 这里只进行了声明,但并没有做任何定义;
  3. 模板特化声明要放在通用类型的声明与定义之后,否则编译器不认识这个声明。

这时,我们将在头文件以外实现它的定义,此处放到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;
}

结果如下:
模板特化(上)

Last modification:April 12, 2021
If you think my article is useful to you, please feel free to appreciate