C++ 模板编程
内容目录

第一章 函数模板

函数模板初探

定义模板

定义一个返回最大值的函数模板

template<typename T>
T max(T a, T b)
{
    return b < a ? a : b;
}

这里T表示类型参数,是模板参数。

使用模板

#include "max1.hpp"
#include <iostream>

int main()
{
    int a = 1, b = 2;
    std::cout << ::max(a, b) << "\n";
    return 0;
}

这里max使用了域限定符::,表示从全局命名空间查找,避免和std中的max函数冲突。

两阶段编译

模板实参推导简介

当我们以实参调用max()等函数模板,模板参数由所传递的实参来决定。
对于:

template<typename T>
T max(const T& a, const T& b)
{
    return b < a ? a : b;
}

int main()
{
    int a = 3, b = 4;
    ::max(a, b);
    return 0;
}

如果传递类型为int的a和b,T仍然推导为int

  1. 类型推导中的类型转换
    • 当声明调用参数是按引用传递时,模板参数T声明的两个实参必须匹配。
    • 当声明调用参数是按值传递时,仅支持退化的简单转换:忽略const和volatile限定符。

例如

template<typename T>
T max(T a, T b)
{
    return b < a ? a : b;
}

int main()
{
    int a = 1;
    const int b = 2;
    max(a, b); // 正确:T推导为int(忽略b的const限定符)

    std::string s;
    max("hello", s); // 错误 T可以推导为const[6]或std::string(表示hello字符串推导有歧义)
    return 0;
}
  1. 默认实参的类型推导
    类型推导对默认调用实参不起作用,例如:
template<typename T>
void f(T a = "");

template<typename T = std::string> // 添加默认实参
void g(T a = "");

int main()
{
    f(1); // 正确推导
    f(); // 错误推导
    g(); // 正确推导
    return 0;
}

多模板参数

模板参数:
template<typename T> // T是模板参数
调用参数
T max(T a, T b) // a和b是调用参数

template<typename T1, typename T2>
T1 max(T1 a, T2 b)
{
    return b < a ? a : b;
}

int main()
{
    std::cout << ::max(3, 3.2) << "\n"; // 输出3
    std::cout << ::max(3.5, 2) << "\n"; // 输出3.5
    return 0;
}

对于上述代码,由于有两个模板参数,并自定义了返回类型,因此在某些情况下,会出现类型转换。

返回类型的模板参数

template<typename T1, typename T2, typename RT>
RT max1(T1 a, T2 b);

template<typename RT, typename T1, typename T2> // 更改模板参数的顺序
RT max2(T1 a, T2 b);

int main()
{
    ::max1<int, double, double>(4, 7.2); // 正确,但需要显式指定返回类型,太麻烦了。
    ::max2<double>(4, 7.2); // 正确,返回类型

}

推导返回类型

通过auto关键字指定返回类型,让编译器自行推导

template<typename T1, typename T2>
auto max(T1 a, T2 b)
{
    return b < a ? a : b;
}

也可以使用尾置返回类型语法

template<typename T1, typename T2>
auto max(T1 a, T2 b) -> decltype(b < a ? a : b)
{
    return b < a ? a : b;
}

注意: 这里的返回类型由?:表达式决定,因为它会返回一个公共的算数类型,例如:

int a = 3;
double b = 3.5;
auto sb = true ? a : b; // 这里a返回的double而不是int

因此,上述模板声明也可如下声明:

template<typename T1, typename T2>
auto max(T1 a, T2 b) -> decltype(true ? a : b);

该定义有显著的缺点:返回类型可能是引用类型
例如:

template <typename T1, typename T2>
auto max(T1 a, T2 b) -> decltype(true ? a : b) {
    return (a > b) ? a : b;
}

int main()
{
    int x = 10;
    int& refX = x;
    int y = 20;
    auto result = max(refX, y); // max会被推导为int&, 但auto初始化时,其类型始终是退化之后的类型,即为int。
    return 0;
}

使用类型特征返回退化后的类型:

template <typename T1, typename T2>
auto max(T1 a, T2 b) -> typename std::decay<decltype(true ? a : b)>::type
{
    return (a > b) ? a : b;
}

int main()
{
    int x = 10;
    int& refX = x;
    int y = 20;
    auto result = max(refX, y); // max会被推导为int
    return 0;
}

返回类型为公共类型

C11开始,可以通过std::common_type<>::type萃取作为模板实参传递的不同类型的公共类型,例如:

template <typename T1, typename T2>
typename std::common_type<T1, T2>::type max(T1 a, T2 b) 
{

    return (a > b) ? a : b;
}

可以将typename std::common_type<T1, T2>::type省略写为std::common_type_t<T1, T2>

默认模板实参简介

可以为模板参数定义默认值,称为默认模板实参。可以将多个实参的公共类型作为返回值类型的默认实参。
方式一:

template <typename T1, typename T2, typename RT = std::decay_t<decltype(true ? T1() : T2())>>
RT max(T1 a, T2 b) 
{
    return (a > b) ? a : b;
}

这里调用了传递类型的默认构造函数。
方式二:
使用过std::common_type<>来指定

template <typename T1, typename T2, typename RT = std::common_type_t<T1, T2>>
RT max(T1 a, T2 b) 
{
    return (a > b) ? a : b;
}

最好的方法让编译器来推导。

重载函数模板简介

template <typename T>
T max(T a, T b) 
{
    return (a > b) ? a : b;
}

int max(int a, int b)
{
    return b < a ? a : b;
}

int main()
{
    ::max(1, 2); // 调用非模板函数
    ::max(7.2, 1.1);
    ::max('a', 'n');
    ::max<>(1, 2);
    ::max<double>(1, 2);
    ::max('a', 1.1); // 调用非模板函数
    return 0;
}

对于两个求最大值的模板:

template <typename T1, typename T2>
auto max(T1 a, T2 b) 
{
    return (a > b) ? a : b;
}

template <typename RT, typename T1, typename T2>
RT max(T1 a, T2 b)
{
    return b < a ? a : b;
}

int main()
{
    auto a = ::max(4, 7.2); //使用第一个模板
    auto b = ::max<long double>(7.2, 4); //使用第二个模板(因为7.2被推导为double,返回值显示指定为long double)
    auto c = ::max<int>(7, 4); // 错误,两个模板都匹配
    return 0;
}

关于为指针和普通的C string 重载这个求最大值的模板:

template <typename T>
T max(T a, T b) 
{
    return (a > b) ? a : b;
}

template <typename T>
T* max(T* a, T* b) 
{
    return (*a > *b) ? a : b;
}

const char* max(const char* a, const char* b) 
{
    return std::strcmp(b, a) < 0 ? a : b;
}

int main()
{
    int a = 7, b = 42;
    auto m1 = ::max(a, b); //1

    std::string s1 = "hey"; 
    std::string s2 = "you";
    auto m2 = ::max(s1, s2); //1

    int* p1 = &b;
    int* p2 = &a;
    auto m3 = ::max(p1, p2); //2

    const char* x = "hello";
    const char* y = "sb";
    auto m4 = ::max(x, y); //3

    return 0;
}

上述都是按值传递的。

关于使用三个实参来比较C string的最大值

template <typename T>
const T& max(const T& a, const T& b) 
{
    std::cout << "1\n";
    return (a > b) ? a : b;
}

const char* max(const char* a, const char* b) 
{
    std::cout << "2\n";
    return std::strcmp(b, a) < 0 ? a : b;
}

// 求三个任意类型值得最大值
template <typename T>
const T& max(const T& a, const T& b, const T& c) 
{
    std::cout << "3\n";
    return ::max(::max(a, b), c);
}

int main()
{
    //auto m1 = ::max(1, 2, 3); //正确

    const char* s1 = "ddfg";
    const char* s2 = "zzxc";
    const char* s3 = "acvbcvbvcb";
    auto m2 = ::max(s1, s2, s3); // 运行时错误

    return 0;
}

为啥会出现运行时错误呢?
调用过程:
::max(s1, s2, s3)会调用第三个模板,返回 ::max(::max(a, b), c),这里首先调用::max(a, b),匹配第二个模板函数:const char max(const char a, const char* b) ,返回值是一个指针(可以先不管它是不是指针,就把它当作一个数值而已,但它的值是如0x......的值,对其解引用后能得到相应字符串的值),然后再次调用第二个模板函数,同样也返回了一个指针。
问题出现了:由于指针也是一个数值,因此三参数模板会产生一个局部变量来存放该指针(数值),而该局部变量被当作引用来返回,导致返回的引用指向了一个临时对象,但该临时对象在函数调用结束后就被销毁了。
这里的临时对象是保存指针的局部变量,指针本身指向的内容不是临时对象,而是因为保存指针的是一个局部变量。

当改变任意顺序时,可能会导致调用顺序不一致:


template <typename T>
T max(T a, T b) 
{
    std::cout << "1\n";
    return (a > b) ? a : b;
}

template <typename T>
T max(T a, T b, T c) 
{

    return ::max(::max(a, b), c);
}

int max(int a, int b)
{
    std::cout << "2\n";
    return (a > b) ? a : b;
}

int main()
{
    ::max(47, 11, 33); // 不会调用普通函数max,因为它的声明太靠后了,导致三参数模板没使用
    return 0;
}

第二章 类模板

类模板Stack的实现

template<typename T>
class Stack
{
private:
    std::vector<T> elems;

public:
    void push(const T& elem); //压入元素
    void pop(); // 弹出元素
    const T& top() const;
    bool empty() const 
    {
        return elems.empty();
    }
};

template<typename T>
void Stack<T>::push(const T& elem)
{
    elems.push_back(elem);
}

template<typename T>
void Stack<T>::pop()
{
    assert(!elems.empty());
    elems.pop_back();
}

template<typename T>
const T& Stack<T>::top() const
{
    assert(!elems.empty());
    return elems.back();
}

类模板的声明

Stack(const Stack&); // 拷贝构造
Stack& operator=(const Stack&);

等同于
Stack(const Stack<T>&);
Stack<T>& operator=(const Stack<T>&);

成员函数的实现

类模板Stack的使用

《C++ Templates》第二版中文版。【美】David Vandevoorde著
上一篇
下一篇