C++ 泛型编程指南 非类型模板参数

对于函数和类模板来说,模板参数可以是类型,也可以是普通值。与使用类型参数的模板一样, 定义在使用之前。使用这样的模板时,必须显式地指定值。然后,实例化生成的代码。本章演示了 新版栈类模板的特性。此外,还会展示非类型函数模板参数的示例,并讨论了这种技术的限制。

1. 类模板参数

与前几章的栈实现不同,这里可以通过使用固定大小的元素数组来实现栈。这种方法的优点是 避免了内存管理开销,但这为堆栈确定最佳容量带来了困难。指定的容量越小,堆栈越有可能快速 填满。指定的容量越大,有可能会浪费内存。好的解决方案是让堆栈的用户指定数组的大小,作为 堆栈的最大容量。

为此,可以将 size 定义为模板参数:

这里是你提供的代码及其中文注释和解释:

#include <array>
#include <cassert>
#include <string>
#include <iostream>

// 模板类定义:实现一个栈,最大容量由模板参数指定
template<typename T, std::size_t MaxSize>
class Stack {
private:
    std::array<T, MaxSize> elems;   // 用std::array保存栈中的元素
    std::size_t numElems;           // 当前栈中元素的数量

public:
    Stack();                        // 构造函数

    void push(T const& elem);       // 压入元素到栈
    void pop();                     // 弹出栈顶元素
    T const& top() const;           // 返回栈顶元素

    bool empty() const {            // 判断栈是否为空
        return numElems == 0;
    }

    std::size_t size() const {      // 返回当前栈中元素的数量
        return numElems;
    }
};

// 构造函数初始化
template<typename T, std::size_t MaxSize>
Stack<T, MaxSize>::Stack()
    : numElems(0)                       // 初始时元素数量为0
{
    // 无需其他初始化
}

// 压入元素到栈
template<typename T, std::size_t MaxSize>
void Stack<T, MaxSize>::push(T const& elem)
{
    assert(numElems < MaxSize);     // 确保栈没有溢出
    elems[numElems] = elem;         // 添加元素到栈顶
    ++numElems;                     // 更新元素数量
}

// 从栈顶弹出元素
template<typename T, std::size_t MaxSize>
void Stack<T, MaxSize>::pop()
{
    assert(!empty());               // 确保栈不为空
    --numElems;                     // 减少元素数量
}

// 返回栈顶元素
template<typename T, std::size_t MaxSize>
T const& Stack<T, MaxSize>::top() const
{
    assert(!empty());               // 确保栈不为空
    return elems[numElems - 1];     // 返回栈顶元素
}

int main() {
    Stack<int, 20> int20Stack;       // 定义一个可存储20个整数的栈
    Stack<int, 40> int40Stack;       // 定义一个可存储40个整数的栈
    Stack<std::string, 40> stringStack; // 定义一个可存储40个字符串的栈

    // 操作20个整数的栈
    int20Stack.push(7);             // 压入整数7
    std::cout << int20Stack.top() << '\n';  // 输出栈顶元素
    int20Stack.pop();               // 弹出栈顶元素

    // 操作40个字符串的栈
    stringStack.push("hello");      // 压入字符串"hello"
    std::cout << stringStack.top() << '\n'; // 输出栈顶元素
    stringStack.pop();              // 弹出栈顶元素
}

代码解释:

  1. 模板类 Stack:

    • 使用 C++ 模板定义,一个类型参数 T 用来指定栈中元素的类型,一个无符号整数 MaxSize 用来指定栈的最大容量。
    • 内部使用 std::array 存储元素,这样可以在编译时确定栈的最大容量。
    • 提供了基本的栈操作:push(压入),pop(弹出),top(获取栈顶元素),以及检查栈是否为空的 empty 方法。
  2. 构造函数:

    • 初始化 numElems 为0,表示栈开始时没有元素。
  3. 成员函数:

    • push 方法在向栈中压入新元素之前使用 assert 确保栈未溢出。
    • poptop 方法则使用 assert 确保操作前栈不为空。
  4. main 函数:

    • 创建了用于测试的多个 Stack 实例。
    • 对整数栈和字符串栈都进行了基本操作测试。

这个类提供了一个简单而有效的方式去实现一个带有固定最大容量的栈,充分使用了 C++ 的模板和断言特性来进行类型安全的编程以及运行时检查。


第二个模板形参 Maxsize 是 std::size_t 类型,指定了堆栈元素内部数组的容量.

template<typename T, std::size_t Maxsize> class Stack { private: std::array<T, Maxsize> elems; // elements
}

另外,在 push() 中,可以使用它来检查堆栈是否已满:

void push(T const& elem) { assert(numElems < Maxsize); // check if the stack is full elems[numElems] = elem; // append element ++numElems; // increment number of elements }

每个模板实例化都是独立的类型。因此,int20Stack 和 int40Stack 是两种不同的类型。不能使用 其中一个来代替另一个,也不能将其中一个分配给另一个。

同样,可以指定模板参数的默认值:

template<typename T = int , std::size_t MaxSize=100>
class Stack {
private:
}

2.函数模板参数

// 文件: basics/addvalue.hpp
template<int Val, typename T>
T addValue(T x) {
    return x + Val;
}

// 使用示例
#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> source = {1, 2, 3, 4, 5};
    std::vector<int> dest(source.size());

    // 使用 std::transform 为 source 每个元素加 5,然后存入 dest
    std::transform(source.begin(), source.end(), dest.begin(), addValue<5, int>);

    // 打印结果
    for (int value : dest) {
        std::cout << value << " ";
    }

    return 0;
}

在这个例子中,我们定义了一个函数模板 addValue,它接受一个整型非类型参数 Val 和一个类型参数 T。我们使用 std::transformsource 中的元素加上 5,并存入 dest 向量中。

接下来是用于后面说明的两个模板定义示例:

template<auto Val, typename T = decltype(Val)>
T foo();

// 确保传递的值与传递的类型相同
template<typename T, T Val = T{}>
T bar();

这些模板展示了更高级的C++模板编程技巧,如通过 decltype 推导返回类型以及确保值和类型参数的一致性。.

以下是重新排版后的内容,包含模板参数限制的相关说明和示例代码:


3. 模板参数的限制

非类型模板参数有一些限制,它们只能是以下类型的值:

  • 整型常量值(包括枚举)
  • 指向对象/函数/成员的指针
  • 指向对象或函数的左值引用
  • std::nullptr_t(即 nullptr 的类型)

浮点数和类型对象不允许作为非类型模板参数:

template<double VAT> // 错误:浮点数不能作为模板参数
double process(double v) {
    return v * VAT;
}

template<std::string name> // 错误:类类型对象不能作为模板参数
class MyClass {
    // ...
};

当为指针或引用传递模板参数时,指向的对象不能是字符串字面值、临时对象或数据成员等。在 C++17 前,这些限制更严格:

  • C++11:对象必须具有外部链接。
  • C++14:对象必须具有外部或内部链接。

以下示例演示这些限制:

template<char const* name>
class Message {
    // ...
};

// 错误:字符串字面值 "hello" 不被允许
Message<"hello"> x;

但是,可以通过以下方式解决问题(取决于 C++ 的版本):

extern char const s03[] = "hi";    // 外部链接
char const s11[] = "hi";           // 内部链接

int main() {
    Message<s03> m03; // 在所有版本中都可以
    Message<s11> m11; // 从C++11起有效

    static char const s17[] = "hi"; // 无链接
    Message<s17> m17; // 从C++17起有效
}

在这三种情况下,常量字符数组都由 "hi" 初始化,该对象作为 char const* 类型的模板参数。对象具有外部链接(s03)在所有C++版本中有效;对象具有内部链接(s11)在C++11和C++14中有效;对象没有链接则需要C++17的支持。


请参阅第 12.3.3 节和第 17.2 节,了解该领域在未来可能的变化。

以下是关于避免无效表达式的重新排版内容,其中包含非类型模板参数和正确使用运算符的示例:


3.1 避免无效的表达式

非类型模板参数可以是编译时的表达式。例如:

template<int I, bool B>
class C;

// 使用编译时表达式赋值给模板参数
C<sizeof(int) + 4, sizeof(int) == 4> c;

在表达式中使用 > 时,必须注意将整个表达式放入圆括号中,让编译器正确判断 > 的结束位置:

C<42, sizeof(int) > 4> c;  // 错误:第一个 `>` 结束了模板参数列表

C<42, (sizeof(int) > 4)> c; // 正确:使用圆括号明确表达式范围

这个示例说明了当在模板参数表达式中使用大于运算符 > 时,如何避免语法错误。通过将表达式用括号括起,可以确保编译器正确理解参数列表的边界。

以下是关于使用 auto 作为模板参数类型的内容重新排版,其中示例展示了如何定义一个确定大小的堆栈类:


4. 模板参数类型 auto

在 C++17 中,可以使用 auto 来定义非类型模板参数。这个特性允许我们创建一个确定大小的堆栈类,例如:

#include <array>
#include <cassert>

// 使用类型 T 和非类型模板参数 Maxsize 定义 Stack 类
template<typename T, auto Maxsize>
class Stack {
public:
    using size_type = decltype(Maxsize);  // Maxsize 的类型

private:
    std::array<T, Maxsize> elems;  // 存储元素的数组
    size_type numElems;            // 当前元素数量

public:
    Stack();                       // 构造函数
    void push(T const& elem);      // 压入元素
    void pop();                    // 弹出元素
    T const& top() const;          // 返回栈顶元素

    bool empty() const {           // 判断栈是否为空
        return numElems == 0;
    }

    size_type size() const {       // 返回栈的当前元素数量
        return numElems;
    }
};

// 构造函数实现
template<typename T, auto Maxsize>
Stack<T, Maxsize>::Stack()
: numElems(0) // 初始化元素数量为 0
{
    // 无需其他操作
}

// push 函数实现
template<typename T, auto Maxsize>
void Stack<T, Maxsize>::push(T const& elem)
{
    assert(numElems < Maxsize);   // 检查栈是否已满
    elems[numElems] = elem;        // 添加元素
    ++numElems;                    // 增加元素数量
}

// pop 函数实现
template<typename T, auto Maxsize>
void Stack<T, Maxsize>::pop()
{
    assert(!empty());             // 检查栈是否为空
    --numElems;                   // 减少元素数量
}

// top 函数实现
template<typename T, auto Maxsize>
T const& Stack<T, Maxsize>::top() const
{
    assert(!empty());             // 检查栈是否为空
    return elems[numElems - 1];   // 返回栈顶元素
}

通过使用 auto 作为占位符类型,Maxsize 可以是任何允许的非类型模板参数类型。内部实现可以同时使用这两个值:

std::array<T, Maxsize> elems;  // 元素存储
using size_type = decltype(Maxsize);  // Maxsize 的类型定义

这个设计允许 Stack 类灵活地处理不同类型和大小的堆栈。

以下是关于使用 auto 确定返回类型的内容重新排版,其中展示了如何在 C++14 后使用 auto 作为返回类型,以及如何基于模板参数类型进行不同的操作:


在 C++14 之后,可以在类的方法中使用 auto 作为返回类型,编译器将自动确定返回类型。以下是一个例子:

// 使用 auto 作为 size() 的返回类型
auto size() const {
    return numElems; // 返回当前元素数量
}

通过这个类的声明,当我们在使用堆栈时,元素数量的类型取决于模板参数的类型定义。例如:

// 文件: basics/stackauto.cpp

#include <iostream>
#include <string>
#include "stackauto.hpp"

int main() {
    Stack<int, 20u> int20Stack;  // 最多可容纳 20 个 int 的栈
    Stack<std::string, 40> stringStack;  // 最多可容纳 40 个字符串的栈

    // 操作 int 类型栈(最多 20 个元素)
    int20Stack.push(7);
    std::cout << int20Stack.top() << '\n';
    auto size1 = int20Stack.size();

    // 操作 string 类型栈(最多 40 个元素)
    stringStack.push("hello");
    std::cout << stringStack.top() << '\n';
    auto size2 = stringStack.size();

    // 检查 int20Stack 和 stringStack 的 size() 返回类型是否相同
    if (!std::is_same_v<decltype(size1), decltype(size2)>) {
        std::cout << "size types differ" << '\n';
    }
}

在这个例子中:

Stack<int, 20u> int20Stack;  // 最多可容纳 20 个 int 的栈

由于传递了 20u,因此内部的大小类型为 unsigned int

Stack<std::string, 40> stringStack;  // 最多可容纳 40 个字符串的栈

由于传递了 40,因此内部的大小类型为 int

因此,两个堆栈的 size() 返回的类型不同:

auto size1 = int20Stack.size();
auto size2 = stringStack.size();

size1size2 的类型会有所不同。我们可以使用标准类型特征 std::is_samedecltype 进行检查:

if (!std::is_same_v<decltype(size1), decltype(size2)>) {
    std::cout << "size types differ" << '\n';
}

因此,程序的输出将是:

size types differ

通过这种方式,我们可以灵活地在模板中处理不同类型参数和非类型参数组合。

以下是关于 C++17 后的一些新特性重新排版的内容,展示了如何简化类型特征的使用和更灵活的模板参数使用:


在 C++17 之后,可以使用 _v 后缀来代替使用 ::value,这使代码更简洁。以下是一个示例:

// 比较 size1 和 size2 的类型是否相同
if (!std::is_same_v<decltype(size1), decltype(size2)>) {
    std::cout << "size types differ" << '\n';
}

对非类型模板参数的限制仍然适用。尤其是,非类型模板参数不能是浮点数类型。例如:

Stack<int, 3.14> sd;  // 错误:浮点数类型的非类型参数

在 C++17 中,可以通过 auto 接受任意类型的非类型参数,还可以传递字符串作为常量数组(甚至可以是静态的局部声明)。例如:

// 文件: basics/message.cpp

#include <iostream>

// 接受任意可能的非类型参数值(自 C++17 开始)
template<auto T>
class Message {
public:
    void print() {
        std::cout << T << '\n';
    }
};

int main() {
    Message<42> msg1;
    msg1.print();  // 使用 int 42 进行初始化并打印此值

    static char const s[] = "hello";
    Message<s> msg2;  // 使用 char const[6] "hello" 进行初始化
    msg2.print();     // 打印该值
}

此外,使用 template<decltype(auto) N> 可以支持更复杂的类型推导,这也允许将模板参数实例化为引用。例如:

template<decltype(auto) N>
class C {
    // ...
};

int i;
C<(i)> x;  // N 是 int&

关于更多细节可以参考第 15.10.1 节。这种方式允许模板参数拥有更多的灵活性和应用场景。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

丁金金

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值