C++ 泛型编程指南 类模板,类型推断,特化,别名模板

文章目录

1.声明一个类模板

声明类模板类似于声明函数模板。在定义具体内容之前,需要先声明一个或多个作为模板类型参数的标识符。通常,这个标识符用 T 表示:

template<typename T>
class Stack {
    // 类的内容
};

在这里,可以用关键字 class 代替 typename

template<class T>
class Stack {
    // 类的内容
};

在类模板内部,T 可以像普通类型一样用于声明成员变量和成员函数。在这个例子中,T 被用于声明 vector 中元素的类型,用于声明成员函数 push() 的参数类型,也被用于成员函数 top 的返回类型:

template<typename T>
class Stack {
private:
    std::vector<T> elems; // 元素
public:
    void push(T const& elem); // 压入元素
    void pop();               // 弹出元素
    T const& top() const;     // 返回顶部元素
    bool empty() const {      // 返回栈是否为空
        return elems.empty();
    }
};

这个类的类型是 Stack<T>,其中 T 是模板参数。在声明时,除非可以推断出模板参数的类型,否则必须使用 Stack<T>Stack 后面必须跟着 <T>)。不过,如果在类模板内部使用 Stack 而不是 Stack<T>,则表示这个内部类的模板参数类型和模板类的参数类型相同。

例如,如果需要定义自己的复制构造函数和赋值构造函数,通常应定义如下:

template<typename T>
class Stack {
    // ...
    Stack(Stack const&);                 // 复制构造函数
    Stack& operator=(Stack const&);      // 赋值操作符
    // ...
};

这等效于下面的定义:

template<typename T>
class Stack {
    // ...
    Stack(Stack<T> const&);              // 复制构造函数
    Stack<T>& operator=(Stack<T> const&); // 赋值操作符
    // ...
};

通常 <T> 暗示对某些模板参数做特殊处理,所以最好还是使用第一种方式。但是如果在类模板的外面,就需要这样定义(完整的 Stack<T> 而不是Stack ``):

template<typename T>
bool operator==(Stack<T> const& lhs, Stack<T> const& rhs);

注意:在只需要类的名字而不是类型的地方,可以只用 Stack。这和声明构造函数和析构函数的情况相同。 参见 [[#构造函数的函数名称不要使用 模板参数 stack<T>]]
另外,与非模板类不同,不可以在函数内部或者块作用域内({...})声明和定义模板。通常模板只能定义在 global/namespace 作用域,或者是其它类的声明里面。
原因参见 [[#模板必须在全局或命名空间作用域,或者类的范围内定义]]

当然,这里是经过排版后的总结:


2.模板类成员函数实现

  1. 模板定义必要性

    • 在实现类模板的成员函数时,必须指明它是一个模板类,并使用该模板类的所有类型参数。这确保了模板被正确识别和使用。 [[#为什么需要指明模板类?]]
  2. 成员函数push

    • 功能:将元素追加到Stack的内部vector中。
    • 实现
      template<typename T>
      void Stack<T>::push(T const& elem) {
          elems.push_back(elem); // append copy of passed elem
      }
      
    • 说明:利用vectorpush_back方法追加元素到尾部。
  3. 成员函数pop

    • 功能:删除并返回栈顶元素。
    • 实现
      template<typename T>
      T Stack<T>::pop() {
          assert(!elems.empty());
          T elem = elems.back(); // save copy of last element
          elems.pop_back(); // remove last element
          return elem; // return copy of saved element
      }
      
    • 说明
      • 使用局部变量elem保存最后一个元素的副本。
      • 使用assert确保栈不为空,因为在vector为空时,backpop_back的行为未定义。

注意 vector 的 pop_back()方法只是删除掉尾部的元素,并不会返回这一元素。这主要是为了异常安全(exception safety)。实现一个异常安全并且能够返回被删除元素的pop()方法是不可 能 的
不过如果忽略掉这一风险,我们依然可以实现一个返回被删除元素的 pop()。为了达到这一目的,我们只需要用T 定义一个和vector 元素有相同类型的局部变量就可以了:

参见 [[#这段文字讨论了在使用标准库中的std::stack时,如何避免因异常导致数据丢失的问题]]

  1. 成员函数top

    • 功能:返回栈顶元素(但不删除)。
    • 实现
      template<typename T>
      T const& Stack<T>::top() const {
          assert(!elems.empty());
          return elems.back(); // return copy of last element
      }
      
    • 说明:同样使用assert来检查栈是否为空。
  2. 内联实现

    • 类模板的成员函数可以直接在类定义中实现为内联函数:
      template<typename T>
      class Stack {
          // ... 
          void push(T const& elem) {
              elems.push_back(elem); // append copy of passed elem
          }
          // ... 
      };
      
    • 说明:内联实现可以优化性能,减少函数调用的开销。

完整代码

对于 pop 只删除不返回,同事 empty 使用内联方式

 #include <vector>
#include <cassert>

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

public:
    void push(T const& elem); // push element
    void pop();               // pop element
    T const& top() const;     // return top element
    bool empty() const {      // return whether the stack is empty
        return elems.empty();
    }
};

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

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

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

// Example usage
int main() {
    Stack<int> intStack;
    intStack.push(1);
    intStack.push(2);
    intStack.push(3);

    while (!intStack.empty()) {
        std::cout << intStack.top() << std::endl; // Output the top element
        intStack.pop();
    }

    return 0;
}

忽略 pop 的异常安全风险,可以这样修改。

#include <vector>
#include <cassert>
#include <stdexcept> // 用于 std::out_of_range

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

public:
    void push(T const& elem); // 压入元素
    T pop();                  // 弹出元素并返回它
    T const& top() const;     // 返回顶部元素
    bool empty() const {      // 判断堆栈是否为空
        return elems.empty();
    }
};

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

template<typename T>
T Stack<T>::pop() {
    if (elems.empty()) {
        throw std::out_of_range("Stack<>::pop(): empty stack");
    }
    T const& topElem = elems.back(); // 获取顶部元素
    elems.pop_back();                // 弹出顶部元素
    return topElem;                  // 返回复制的顶部元素
}

template<typename T>
T const& Stack<T>::top() const {
    if (elems.empty()) {
        throw std::out_of_range("Stack<>::top(): empty stack");
    }
    return elems.back();
}

// 示例用法
#include <iostream>

int main() {
    Stack<int> intStack;
    intStack.push(1);
    intStack.push(2);
    intStack.push(3);

    try {
        while (!intStack.empty()) {
            std::cout << intStack.pop() << std::endl; // 输出并弹出顶部元素
        }
    }
    catch (const std::out_of_range& e) {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}

3. 使用 Stack 类模板

直到 C++17,使用类模板时需要显式地指明模板参数。下面的例子展示了如何使用 Stack<> 类模板:

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

int main() {
    Stack<int> intStack;           // stack of ints
    Stack<std::string> stringStack; // stack of strings

    // manipulate int stack
    intStack.push(7);
    std::cout << intStack.top() << '\n';

    // manipulate string stack
    stringStack.push("hello");
    std::cout << stringStack.top() << '\n';
    stringStack.pop();
}

通过声明 Stack<int> 类型,类模板内部的 int 会被用作类型 T。创建的 intStack 会使用一个存储 intvector 作为其 elems 成员,所有被用到的成员函数都会被用 int 实例化。同样的,对于使用 Stack<std::string> 定义的对象,它会使用一个存储 std::stringvector 作为其 elems 成员,所有被用到的成员函数也都会用 std::string 实例化。

注意,模板函数和模板成员函数只有在被调用的时候才会实例化。这一设计节省了时间和空间,同时允许只部分使用类模板,我们会在 2.3 节对此进行讨论。在这个例子中,对于 intstd::string,默认构造函数、push() 以及 top() 函数都会被实例化。而 pop() 只会针对 std::string 实例化。如果一个类模板有 static 成员,对每一个用到这个类模板的类型,相应的静态成员也只会被实例化一次。

被实例化之后的类模板类型(如 Stack<int>)可以像其它常规类型一样使用。可以用 const 以及 volatile 修饰它,或者用它来创建数组和引用。可以通过 typedefusing 将它用于类型定义的一部分(关于类型定义,请参见 2.8 节),也可以用它来实例化其他的模板类型。例如:

void foo(Stack<int> const& s) { // parameter s is int stack
    using IntStack = Stack<int>; // IntStack is another name for Stack<int>
    Stack<int> istack[10];       // istack is array of 10 int stacks
    IntStack istack2[10];        // istack2 is also an array of 10 int stacks (same type)
}

模板参数可以是任意类型,比如指向 float 的指针,甚至是存储 intstack

Stack<float*> floatPtrStack;        // stack of float pointers
Stack<Stack<int>> intStackStack;    // stack of stacks of ints

模板参数唯一的要求是:它要支持模板中被使用的各种操作(运算符)。在 C++11 之前,两相邻的模板尖括号之间必须要有空格:

Stack<Stack<int> > intStackStack;   // OK with all C++ versions

如果不这样做,>> 会被解析成调用 >> 运算符,这会导致语法错误:

Stack<Stack<int>> intStackStack;    // ERROR before C++11

这种要求的原因在于,它帮助编译器在第一次识别源代码时,不依赖于语义就能对源码进行正确的标记。但是由于漏掉空格是一个常见错误,并且这种情况需要合适的错误信息来进行处理,从 C++11 开始,“angle bracket hack” 技术(详见 13.3.1 节)使得在两个相邻的模板尖括号之间不再必须使用空格。


4.部分使用类模板

一个类模板通常会对用来实例化它的类型进行多种操作(包含构造函数和析构函数)。这可能会让你以为,要为模板参数提供所有被模板成员函数用到的操作。但是事实不是这样:模板参数只需要提供那些会被用到的操作(而不是可能会被用到的操作)。

增加一个函数用于打印 stack 中的元素。

#include <vector>
#include <cassert>
#include <stdexcept> // 用于 std::out_of_range
#include <iostream>
using std::cout;
using std::cin;

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

public:
    void push(T const& elem); // 压入元素
    T pop();                  // 弹出元素并返回它
    T const& top() const;     // 返回顶部元素
    bool empty() const {      // 判断堆栈是否为空
        return elems.empty();
    }
    void printOn(std::ostream& strm) const {
        for (T const& elem : elems) {
            strm << elem << " "; // 这里建议加一个空格,以便元素之间有间隔,不然会连在一起
        }
    }
};

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

template<typename T>
T Stack<T>::pop() {
    if (elems.empty()) {
        throw std::out_of_range("Stack<>::pop(): empty stack");
    }
    T const& topElem = elems.back(); // 获取顶部元素
    elems.pop_back();                // 弹出顶部元素
    return topElem;                  // 返回复制的顶部元素
}

template<typename T>
T const& Stack<T>::top() const {
    if (elems.empty()) {
        throw std::out_of_range("Stack<>::top(): empty stack");
    }
    return elems.back();
}

// 示例用法
#include <iostream>

int main() {
    Stack<int> intStack;
    intStack.push(1);
    intStack.push(2);
    intStack.push(3);

    intStack.printOn(std::cout);
     return 0;
}

只要栈中的类型 T 支持标准输出运算符 <<,那么 printOn 就可以正常工作。

为了体现 模板参数只需要提供那些会被用到的操作 , 我们提供一个 类型 T 不支持标准输出运算符 <<

    Stack<std::pair< int, int>> ps; // note: std::pair<> has no operator<<defined
    ps.push({ 4, 5 }); // OK
    ps.push({ 6, 7 }); // OK
    std::cout << ps.top().first <<'\n'; // OK
    std::cout << ps.top().second <<'\n'; // OK
       ps.printOn(std::cout); // ERROR: operator<< not supported for elementtype
   return 0;

只有在调用 printOn()的时候,才会导致错误,因为它无法为这一类型实例化出对operator<<的调用

printOn 方法报错是因为 std::pair 没有定义适用于 operator<< 的输出运算符。在代码中,printOn 方法试图对每一个存在于栈中的元素使用 operator<< 进行输出。然而,std::pair<int, int> 类型的元素没有内置的 operator<< 支持,因此会导致编译错误。


解决这个问题的方法是定义一个适用于 std::pair 的自定义 operator<<。定义这个运算符使得当需要输出 std::pair 的对象时,编译器知道如何处理。在你的代码中,可以这样添加

#include <vector>
#include <cassert>
#include <stdexcept>  // 用于 std::out_of_range
#include <iostream>
#include <utility>    // for std::pair
using std::cout;
using std::cin;

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

public:
    void push(T const& elem); // 压入元素
    T pop();                  // 弹出元素并返回它
    T const& top() const;     // 返回顶部元素
    bool empty() const {      // 判断堆栈是否为空
        return elems.empty();
    }
    void printOn(std::ostream& strm) const {
        for (T const& elem : elems) {
            strm << elem << " "; // 这里建议加一个空格,以便元素之间有间隔,不然会连在一起
        }
    }
};

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

template<typename T>
T Stack<T>::pop() {
    if (elems.empty()) {
        throw std::out_of_range("Stack<>::pop(): empty stack");
    }
    T const& topElem = elems.back(); // 获取顶部元素
    elems.pop_back();                // 弹出顶部元素
    return topElem;                  // 返回复制的顶部元素
}

template<typename T>
T const& Stack<T>::top() const {
    if (elems.empty()) {
        throw std::out_of_range("Stack<>::top(): empty stack");
    }
    return elems.back();
}

template<typename T1, typename T2>
std::ostream& operator<<(std::ostream& os, const std::pair<T1, T2>& pair) {
    return os << '(' << pair.first << ", " << pair.second << ')';
}

// 示例用法
int main() {
    Stack<std::pair<int, int>> ps; // note: std::pair<> has no operator<< defined
    ps.push({ 4, 5 }); // OK
    ps.push({ 6, 7 }); // OK
    std::cout << ps.top().first << '\n'; // OK
    std::cout << ps.top().second << '\n'; // OK
    ps.printOn(std::cout); // OK, Now operator<< is supported for element type
    return 0;
}

5.模板类的特例化

可以为某些模板参数特化类模板。类似于函数模板的重载 (参见第 1.5 节),特化类模板允许优 化特定类型的实现,或者为类模板的实例化修复特定类型的错误行为。

但若特化类模板,则必须特 化所有成员函数。虽然可以特化类模板的单个成员函数,但若这样做了,就不能再特化该特化成员 所属的整个类模板实例。 理解这句话参见[[#但若特化类模板,则必须特 化所有成员函数。虽然可以特化类模板的单个成员函数,但若这样做了,就不能再特化该特化成员 所属的整个类模板实例。]]

为了特化一个类模板,在类模板声明的前面需要有一个 template<>,并且需要指明希望特化的类型。这些用于特化类模板的类型被用作模板参数,并且需要紧跟在类名的后面:

template<>
class Stack<std::string> { 
    // ...
};

对于被特化的模板,所有成员函数的定义都应该被定义成“常规”成员函数,即所有出现 T 的地方,都应该被替换成用于特化类模板的类型:

void Stack<std::string>::push (std::string const& elem) {
    elems.push_back(elem); // append copy of passed elem
}

下面是一个用 std::string 实例化 Stack<> 类模板的完整例子:

 
#include <vector>
#include<deque>
#include <cassert>
#include <stdexcept> // 用于 std::out_of_range
#include <iostream>
#include <cassert>
using std::cout;
using std::cin;
//主模板
template<typename T>
class Stack {
private:
	std::vector<T> elems; // 元素

public:
	void push(T const& elem) { elems.push_back(elem); }; // 压入元素
	T pop() {
		if (elems.empty()) {
			throw std::out_of_range("Stack<>::pop(): empty stack");
		}
		T const& topElem = elems.back(); // 获取顶部元素
		elems.pop_back();                // 弹出顶部元素
		return topElem;                  // 返回复制的顶部元素
	};                  // 弹出元素并返回它
	T const& top() const {
		if (elems.empty()) {
			throw std::out_of_range("Stack<>::top(): empty stack");
		}
		return elems.back();
	};     // 返回顶部元素
	bool empty() const {      // 判断堆栈是否为空
		return elems.empty();
	}
	void printOn(std::ostream& strm) const {
		for (T const& elem : elems) {
			strm << elem << " "; // 这里建议加一个空格,以便元素之间有间隔,不然会连在一起
		}
	}
};
//特化版本
template<>
class Stack<std::string> {
	std::deque<std::string> elems;
public:
	void push(const std::string&);
	void pop() { assert(!elems.empty()); elems.pop_back(); }
	const std::string& top() const { assert(!elems.empty()); return elems.back(); }
	bool empty() const { return elems.empty(); }
};

void Stack<std::string>::push(const std::string& elem) {
	elems.push_back(elem);
}



// 函数定义:演示 Stack<std::string> 的基本操作
void testStringStack() {
	Stack<std::string> stringStack; // 创建一个 Stack<std::string> 对象

	// 使用 push() 方法添加元素
	stringStack.push("Hello");
	stringStack.push("World");

	// 使用 top() 方法获取并输出顶部元素
	std::cout << "Top element: " << stringStack.top() << std::endl; // 输出: World

	// 使用 pop() 方法移除顶部元素
	stringStack.pop();

	// 再次获取并输出顶部元素
	std::cout << "Top element after pop: " << stringStack.top() << std::endl; // 输出: Hello

	// 检查并输出堆栈是否为空
	if (!stringStack.empty()) {
		std::cout << "The stack is not empty." << std::endl;
	}
	else {
		std::cout << "The stack is empty." << std::endl;
	}
}
void testIntStack() {
	Stack<int> intStack;
	intStack.push(1);
	intStack.push(2);
	intStack.push(3);

	intStack.printOn(std::cout);
}
int main() {
	std::cout << '\n' << "testIntStack" << std::endl;
	testIntStack();//演示主模板
	std::cout << '\n' << "testStringStack" << std::endl;
	testStringStack();//演示特化的模板
	return 0;
}

在这个例子中,特例化之后的类在向 push() 传递参数时使用了引用语义,对于当前 std::string 类型,这是有意义的,因为这可以提高性能(如果使用转发引用传递参数的话会更好一些,6.1节会介绍这一内容)。

另一个不同是使用了 deque 而不是 vector 来存储 stack 里面的元素。虽然这样做可能不会有什么好处,但这能够说明,模板类特例化之后的实现可能和模板类的原始实现有很大不同。


6. 模板类的偏特化

上面的那种特例化方式是 全特化 , 还有 偏特化

6.1 区分偏特化和全特化

6.1.1 偏特化

在C++中,模板的偏特化(部分特化)提供了一种在保持模板大部分通用性的同时处理某些特殊情况下的特定行为的能力(简单来说,你对模板的某些方面做了特定的处理,但仍然保留了一些模板参数的通用性(存在模板参数)。)。以下是一些常见的偏特化类别及其应用示例:

  1. 类模板的部分特化

    • 指针类型特化:为指针类型提供特定实现。
      template<typename T>
      class MyClass {
          // 通用实现
      };
      
      // 针对指针类型的偏特化
      template<typename T>
      class MyClass<T*> {
          // 对指针类型的特殊实现
      };
      
  2. 特定数值的特化

    • 特定尺寸的数组:处理特定长度的数组。
      template<typename T, size_t N>
      class Array {
          // 通用实现
      };
      
      template<typename T>
      class Array<T, 10> {
          // 专门处理大小为10的数组
      };
      
  3. 类型性质的特化

    • 区分const和非const类型
      template<typename T>
      class MyClass {
          // 非const类型的实现
      };
      
      template<typename T>
      class MyClass<const T> {
          // const类型的特别实现
      };
      
  4. 多个模板参数的特化

    • 部分特化多个参数
      template<typename T, typename U>
      class Pair {
          // 通用实现
      };
      
      template<typename T>
      class Pair<T, int> {
          // 当第二个模板参数为int时的特别实现
      };
      

这些偏特化的例子展示了如何利用C++模板的灵活性,针对特定情况优化性能或实现特定行为,同时保持代码的通用性和可复用性。使用偏特化可以在不影响整体设计的情况下提高代码的效率和可读性。

6.1.2 全特化

全特化(完全特化)是指为模板类或模板函数的特定类型参数组合提供一个完全定制的实现。在全特化中,模板参数的类型被完整地指定。这与偏特化不同,全特化不再保留模板参数的通用性,而是针对特定类型提供实现。以下是几个全特化的常见类别及其示例:

  1. 类模板的全特化

    • 特定类型的全特化
      template<typename T>
      class MyClass {
          // 通用实现
      };
      
      // MyClass的特定类型全特化
      template<>
      class MyClass<int> {
          // 针对int类型的实现
      };
      
  2. 多参数类模板的全特化

    • 特定类型组合的全特化
      template<typename T, typename U>
      class Pair {
          // 通用实现
      };
      
      // Pair的特定类型组合全特化
      template<>
      class Pair<int, double> {
          // int和double组合的全特化实现
      };
      
  3. 类模板指针类型的全特化

    • 针对于特定的指针类型
      template<typename T>
      class PointerWrapper {
          // 通用实现
      };
      
      // 针对int指针的全特化
      template<>
      class PointerWrapper<int*> {
          // int指针的特殊实现
      };
      

6.2 偏特化 例子

6.2.1 指针类型偏特化
#include <vector>
#include<deque>
#include <cassert>
#include <stdexcept> // 用于 std::out_of_range
#include <iostream>
#include <cassert>
using std::cout;
using std::cin;
//主模板
template<typename T>
class Stack {
private:
	std::vector<T> elems; // 元素

public:
	void push(T const& elem) { elems.push_back(elem); }; // 压入元素
	T pop() {
		if (elems.empty()) {
			throw std::out_of_range("Stack<>::pop(): empty stack");
		}
		T const& topElem = elems.back(); // 获取顶部元素
		elems.pop_back();                // 弹出顶部元素
		return topElem;                  // 返回复制的顶部元素
	};                  // 弹出元素并返回它
	T const& top() const {
		if (elems.empty()) {
			throw std::out_of_range("Stack<>::top(): empty stack");
		}
		return elems.back();
	};     // 返回顶部元素
	bool empty() const {      // 判断堆栈是否为空
		return elems.empty();
	}
	void printOn(std::ostream& strm) const {
		for (T const& elem : elems) {
			strm << elem << " "; // 这里建议加一个空格,以便元素之间有间隔,不然会连在一起
		}
	}
};
//偏特化
// 为指针类型特化的 Stack<> 类模板
template<typename T>
class Stack<T*> {
private:
	std::vector<T*> elems;  // 存储在栈中的元素

public:
	// 将元素压入栈中
	void push(T* elem) {
		elems.push_back(elem);
	}

	// 从栈中弹出元素
	T* pop() {
		assert(!elems.empty());  // 确保栈不为空
		T* p = elems.back();  // 获取最后一个元素
		elems.pop_back();  // 移除最后一个元素
		return p;
	}

	// 返回栈顶元素
	T* top() const {
		assert(!elems.empty());  // 确保栈不为空
		return elems.back();  // 返回最后一个元素
	}

	// 返回栈是否为空
	bool empty() const {
		return elems.empty();
	}
};


void testIntStack() {
	Stack<int> intStack;
	intStack.push(1);
	intStack.push(2);
	intStack.push(3);
	intStack.printOn(std::cout);
}

void testPointStack()
{
	Stack<int*> ptrStack;  // 指向整数的指针栈(特化实现)
	// 将动态分配的整数压入栈中
	ptrStack.push(new int{ 42 });
	// 获取栈顶元素并打印其值
	std::cout << *ptrStack.top() << '\n';
	// 弹出栈顶元素并删除它以防止内存泄漏
	delete ptrStack.pop();
}
int main() {
	std::cout << '\n' << "testIntStack" << std::endl;
	testIntStack();//演示主模板
	std::cout << '\n' << "testPointStack" << std::endl;
	testPointStack();
	return 0;
}
6.2.2 多个参数的偏特化
#include <iostream>

// 主模板
template<typename T1, typename T2>
class MyClass {
public:
    void info() {
        std::cout << "主模板 MyClass<T1, T2>\n";
    }
};

// 偏特化:两个模板参数类型相同
template<typename T>
class MyClass<T, T> {
public:
    void info() {
        std::cout << "偏特化 MyClass<T, T>\n";
    }
};

// 偏特化:第二个参数是 int
template<typename T>
class MyClass<T, int> {
public:
    void info() {
        std::cout << "偏特化 MyClass<T, int>\n";
    }
};

// 偏特化:两个模板参数都是指针类型
template<typename T1, typename T2>
class MyClass<T1*, T2*> {
public:
    void info() {
        std::cout << "偏特化 MyClass<T1*, T2*>\n";
    }
};

int main() {
    MyClass<int, float> mif;    // 使用主模板 MyClass<T1, T2>
    mif.info();

    MyClass<float, float> mff;  // 使用偏特化 MyClass<T, T>
    mff.info();

    MyClass<float, int> mfi;    // 使用偏特化 MyClass<T, int>
    mfi.info();

    MyClass<int*, float*> mp;   // 使用偏特化 MyClass<T1*, T2*>
    mp.info();
    return 0;
}

解释:

  1. 主模板MyClass<T1, T2>是类模板的主要版本,适用于大多数参数组合。

  2. 特殊性:

    • MyClass<T, T>:当两个模板参数类型相同时,使用这个偏特化版本。
    • MyClass<T, int>:当第二个模板参数int为时,使用这个偏特化版本。
    • MyClass<T1*, T2*>:当两个模板参数都是指针类型时,使用这个偏特化版本。
  3. 使用实例

    • MyClass<int, float>:使用原始模板。
    • MyClass<float, float>:使用第一个偏特化(参数类型相同)。
    • MyClass<float, int>:使用第二个偏特化(第二个参数为int)。
    • MyClass<int*, float*>:使用第三个偏特化(参数均为指针类型)。

通过在main()函数中调用info()方法,我们可以看到每个实例化对象使用了哪个特定的模板或偏特化版本。

6.3 偏特化匹配歧义

#include <iostream>

// 主模板
template<typename T1, typename T2>
class MyClass {
public:
    void info() {
        std::cout << "主模板 MyClass<T1, T2>\n";
    }
};

// 偏特化:两个模板参数类型相同
template<typename T>
class MyClass<T, T> {
public:
    void info() {
        std::cout << "偏特化 MyClass<T, T>\n";
    }
};

// 偏特化:第二个参数是 int
template<typename T>
class MyClass<T, int> {
public:
    void info() {
        std::cout << "偏特化 MyClass<T, int>\n";
    }
};

// 偏特化:两个模板参数都是指针类型
template<typename T1, typename T2>
class MyClass<T1*, T2*> {
public:
    void info() {
        std::cout << "偏特化 MyClass<T1*, T2*>\n";
    }
};

int main() {
  MyClass<int, int> mi; // ERROR: matches MyClass<T,T>    // and MyClass<T,int>
 MyClass<int*, int*> mip; // ERROR: matches MyClass<T,T>  // and MyClass<T1*,T2*>
    return 0;
}

在C++中,模板偏特化的匹配是基于模板参数的匹配程度。歧义的产生主要是因为两个不同的偏特化都可以适用于同一个模板参数组,而编译器无法确定应该使用哪个偏特化来进行实例化。

具体原因

  1. MyClass<int, int> mi; 的歧义原因

    • MyClass<T, T>:这是一个偏特化,适用于两个模板参数相同的情况。
    • MyClass<T, int>:这是另一个偏特化,适用于第二个模板参数是 int 的情况。

    对于 MyClass<int, int>

    • MyClass<T, T> 可以匹配,因为 T 可以是 int
    • MyClass<T, int> 也可以匹配,因为第一个 T 可以是 int

    因此,两个偏特化都匹配,导致歧义。

  2. MyClass<int*, int*> mip; 的歧义原因

    • MyClass<T, T>:适用于两个模板参数相同的情况。
    • MyClass<T1*, T2*>:适用于两个指针类型的参数。

    对于 MyClass<int*, int*>

    • MyClass<T, T> 可以匹配,因为 T 可以是 int*
    • MyClass<T1*, T2*> 可以匹配,因为 T1T2 都可以是 int

    同样,这导致了两个不同的偏特化都可以匹配,从而出现歧义。

解决方案

为了避免这种歧义问题,通常可以通过引入一个更具体的偏特化来排除此类冲突:

  • 对于相同类型的简单类型参数,可以引入一个新的、更具体的偏特化:

    template<typename T>
    class MyClass<T, T> {
        ...
    };
    
  • 对于相同类型的指针参数,可以引入一个更具体的偏特化来解决冲突:

    template<typename T>
    class MyClass<T*, T*> {
        ...
    };
    

通过这种方式,编译器能够明确选择匹配更具体的偏特化,从而避免歧义。

7.模板类的默认参数

#include <vector>
#include <cassert>
#include <iostream>
#include <deque>
template<typename T, typename Cont = std::vector<T>>
class Stack {
private:
    Cont elems; // elements

public:
    void push(T const& elem); // push element
    void pop(); // pop element
    T const& top() const; // return top element
    bool empty() const { // return whether the stack is empty
        return elems.empty();
    }
};

template<typename T, typename Cont>
void Stack<T, Cont>::push(T const& elem) {
    elems.push_back(elem); // append copy of passed elem
}

template<typename T, typename Cont>
void Stack<T, Cont>::pop() {
    assert(!elems.empty());
    elems.pop_back(); // remove last element
}

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

int main() {
    // stack of ints:
    Stack<int> intStack;

    // stack of doubles using a std::deque<> to manage the elements
    Stack<double, std::deque<double>> dblStack;

    // manipulate int stack
    intStack.push(7);
    std::cout << intStack.top() << '\n';
    intStack.pop();

    // manipulate double stack
    dblStack.push(42.42);
    std::cout << dblStack.top() << '\n';
    dblStack.pop();
}


这段代码实现了一个模板化的栈(Stack)数据结构,其中提供了元素的压入、弹出和查看栈顶元素的功能。这段代码的有趣之处在于它使用了模板默认参数来定义栈的存储容器类型。

代码分析

  • 类模板定义

    template<typename T, typename Cont = std::vector<T>> 
    class Stack {
    private:
        Cont elems; // 元素
    public:
        void push(T const& elem); // 压入元素
        void pop(); // 弹出元素
        T const& top() const; // 返回栈顶元素
        bool empty() const { // 判断栈是否为空
            return elems.empty();
        }
    };
    
    • T 表示栈中存储的元素类型。
    • Cont 是栈使用的容器类型,默认值是 std::vector<T>,这意味着如果不提供其他容器类型,栈将使用 std::vector 来存储元素。
  • 方法实现

    • push 方法:将元素压入到容器的末尾。
    • pop 方法:从容器的末尾弹出元素。在弹出前通过 assert 断言容器非空。
    • top 方法:返回容器的末尾元素。在访问之前使用 assert 断言确保容器不为空。
    • empty 方法:检查栈是否为空,直接调用底层容器的 empty() 方法。
  • main 函数中

    • 创建了两个栈实例,一个是默认使用 std::vector<int> 作为底层容器的 Stack<int>,另一个是显示使用 std::deque<double>Stack<double, std::deque<double>>
    int main() {
        // int 类型的栈:
        Stack<int> intStack;
    
        // 使用 std::deque<> 管理元素的 double 类型栈
        Stack<double, std::deque<double>> dblStack;
    
        // 操作 int 类型栈
        intStack.push(7);
        std::cout << intStack.top() << '\n';
        intStack.pop();
    
        // 操作 double 类型栈
        dblStack.push(42.42);
        std::cout << dblStack.top() << '\n';
        dblStack.pop();
    }
    

模板默认参数

这段代码展示了使用模板默认参数的优雅之处,使得可以灵活选择数据结构的底层存储机制,比如在这里的 Stack 类中,我们可以选择 std::vector 或通过指定 Cont 类型选择 std::deque,从而获得不同的栈实现。

  • 默认情况下,使用 std::vector 来存储元素。
  • 可以通过显式提供模板参数 Cont 改变底层容器类型,比如在这里使用 std::deque

这种方式提升了代码的复用性和灵活性,适应了多种应用场景而无需对代码进行大量修改。


8. 类型别名

在C++中,别名模板(alias template)是一个用于创建对型别或模板的简化引用的机制。它使用了 using 关键字,使我们能够为复杂或长的模板定义一个简化的名称,便于代码的可读性和维护。

8.1 别名模板的语法

别名模板的基本语法如下:

template<typename... Args>
using AliasName = ExistingTemplate<Args...>;

这里,AliasName 是你定义的别名,而 ExistingTemplate<Args...> 是你为其创建别名的实际类型或模板。

8.2 别名模板的用途

  • 简化复杂型别: 如果某个类型的定义非常复杂,通过别名模板可以为其创建一个更为简单的引用。
  • 提高代码可读性: 在使用长模板的地方使用简化的别名,使代码更容易理解。
  • 增强代码可维护性: 当基础模板或型别发生变化时,只需修改别名模板的定义即可。

示例

这里有一个简单的例子,演示如何使用别名模板:

#include <deque>
#include <vector>
#include <iostream>
#include <string>

// 定义一个基本的 stack 模板
template<typename T, typename Container = std::deque<T>>
class Stack {
public:
    void push(const T& elem) { elems.push_back(elem); }
    void pop() { elems.pop_back(); }
    T const& top() const { return elems.back(); }
    bool empty() const { return elems.empty(); }

private:
    Container elems;
};

// 为 std::vector 作为容器类型创建一个别名模板
template<typename T>
using VectorStack = Stack<T, std::vector<T>>;

int main() {
    // 使用默认的 deque 作为容器的 Stack
    Stack<int> defaultStack;
    defaultStack.push(5);
    std::cout << "Top of defaultStack: " << defaultStack.top() << std::endl;

    // 使用 vector 作为容器的 Stack,通过别名模板
    VectorStack<std::string> vectorBasedStack;
    vectorBasedStack.push("Hello");
    std::cout << "Top of vectorBasedStack: " << vectorBasedStack.top() << std::endl;

    return 0;
}

在这个例子中:

  • Stack 是一个模板类,默认使用 std::deque 作为容器。
  • VectorStack 是一个别名模板,用于将 Stack 的容器类型参数指定为 std::vector
  • 通过别名模板,创建了一个基于 std::vectorStack 实例 vectorBasedStack

这样,大大简化了代码,使得复杂类型使用起来更直观。

8.3 标准库使用别名模板

是的,C++14 引入了许多类型特征的后缀 _t,这些后缀是通过别名模板(alias templates)实现的。别名模板是一种简化特定类型使用的方式,使代码更简洁和可读。

在 C++11 中,我们需要这样使用类型特征:

typename std::add_const<T>::type my_const_type;

这可能会显得冗长和复杂。为了解决这个问题,C++14 引入了别名模板。例如,std::add_const_t<T> 就是一个别名模板,它等价于 typename std::add_const<T>::type,所以我们可以这样使用:

std::add_const_t<T> my_const_type;

在 C++14 中,这种模式被用于标准库中的绝大多数类型特征。例如:

  • std::remove_const_t<T> 代替 typename std::remove_const<T>::type
  • std::decay_t<T> 代替 typename std::decay<T>::type

这种修改极大地简化了代码的书写,并提高了可读性。这些别名模板在实际的库中就是通过 using 关键字引入的:

template<typename T>
using add_const_t = typename add_const<T>::type;

这种使用 using 关键字来创建类型别名是 C++11 引入的特性,使得定义类型别名比使用 typedef 更加便捷和灵活,特别是在处理模板时。

#include <type_traits>
#include <iostream>

// 使用 add_const_t 给一个类型添加 const 限定符
template<typename T>
void printConst()
{
    // 为常量对象 a 和 b 提供初始化
    typename std::add_const<T>::type a = T{};
    std::cout << "Using C++11 way: " << typeid(a).name() << std::endl;

    std::add_const_t<T> b = T{};
    std::cout << "Using C++14 way: " << typeid(b).name() << std::endl;
}

int main() {
    // 打印对 `int` 使用 `add_const_t`
    printConst<int>();

    return 0;
}

9. 类模板的类型推导

C++17 前,必须将所有模板参数类型传递给类模板 (除非有默认值)。C++17 后,指定模板参数 的约束放宽了。相反,若构造函数能够推导出所有模板参数 (没有默认值),则可以不用显式定义模 板参数。

9.1 标准库中的自动类型推导

C++17引入CTAD会带来如下好处:

例如:这么复杂的代码

std::vector<FooBar<int, const char*>> obj{a, b, c}; 

我们只需要:

 std::vector obj{a, b, c};

再比如:

std::tuple<int>

可以简化为:

std::tuple t1{1};

所以,咱们今天来看看CTAD。

CTAD全称是Class template argument deduction (CTAD),类模版参数推导,你给定编译器一个推导指南(deduction guide),我们便可以使用这个特性了。

#include <iostream>

// 一个简单的类模板,类似于 std::pair
template <typename T1, typename T2>
class Pair {
public:
    T1 first;
    T2 second;

    Pair(T1 f, T2 s) : first(f), second(s) {}
    void print() const {
        std::cout << "First: " << first << ", Second: " << second << std::endl;
    }
};

int main() {
    // 在 C++17 之前,我们需要明确地指定模板参数类型
    Pair<int, double> p1(1, 2.5);
    p1.print();

    // 在 C++17 及之后,编译器可以自动推导模板参数类型
    Pair p2(1, 2.5); // 自动推导出 Pair<int, double>
    p2.print();

    return 0;
}


9.1.1 排序函数

在 C++ 中,通过使用模板函数,我们可以对不同类型的迭代器进行操作,而无需显式指定这些迭代器的类型。编译器通过模板参数推导来自动确定迭代器的类型。这种特性使得模板函数在操作各种容器时能够更加灵活和简洁,避免了冗余的类型声明。

下面是一个关于如何使用模板函数对 std::vector<int> 进行排序的简单示例:

#include <iostream>
#include <vector>
#include <algorithm> // for std::sort

// 创建一个模板函数,使用随机访问迭代器来排序元素
template <typename RanIt>
void sort(RanIt first, RanIt last) {
    std::sort(first, last);  // 使用标准库的 std::sort 进行排序
}

int main() {
    std::vector<int> v = {5, 2, 9, 1, 5, 6};

    // 在这里调用 sort 函数,编译器会自动推导出 RanIt 是 std::vector<int>::iterator
    sort(v.begin(), v.end());

    // 输出排序后的结果
    for (const auto& value : v) {
        std::cout << value << " ";
    }
    std::cout << std::endl;

    return 0;
}

在这个例子中:

  1. 模板函数 sort 我们定义了一个模板函数 sort,它接收两个迭代器作为参数。

  2. 自动类型推导: 当我们调用 sort(v.begin(), v.end()) 时,编译器会自动推导出 RanItstd::vector<int>::iterator。你不需要明确指定迭代器的类型。

  3. 排序操作: 使用标准库中的 std::sort 函数来对迭代器范围内的元素进行排序。

通过这种方式,模板函数的使用可以在很大程度上提高代码的灵活性和可重用性,同时保持类型安全。编译器自动推导模板参数类型使得代码更加简洁明了。

9.2 自动类型推导(CTAD)

对于简单的代码,能够实现自动推导,不需要自定义推导指引

template <typename T>
class Add {
private:
    T first;
    T second;
public:
    Add() = default;
    Add(T first, T second) : first_{ first }, second_{ second } {}
    T result() const { return first + second; }
};

int main() {
    Add one(1, 2);               // T被推导为int
    Add two{ 1.245, 3.1415 };     // T被推导为double
    Add three = { 0.24f, 0.34f }; // T被推导为float
}

在这个例子中:

  1. C++17 之前: 我们必须明确地指定 Pair<int, double>,即使编译器已经知道参数 12.5 的类型分别是 intdouble

  2. C++17 及之后: 我们只需写 Pair p2(1, 2.5);,编译器会自动推导出正确的模板参数类型 Pair<int, double>

通过支持类模板参数推导,C++17 简化了对象的创建,减少了代码的冗长,同时也降低了出错的可能性(如指定错误的类型)。这一特性可以大大增强代码的可读性和可维护性。

如果代码不能自动推断出类型,需要自定义指引

#include <iostream>
template <typename T, typename U>
struct MyPair {
	T first{};
	U second{};
};
 
int main() {
	MyPair<int, int> p1{ 1, 2 };
	MyPair p2{ 1, 2 };
	std::cout << p1.first << ", " << p1.second << std::endl;
	std::cout << p2.first << ", " << p2.second << std::endl;
	return 0;
}

对于p2,我们便会报错:

no viable constructor or deduction guide for deduction of template arguments of 'MyPair'

那么对于怎么修改呢?

只需要添加deduction guid即可,如下写法即可。

template <typename T, typename U>
MyPair(T, U) -> MyPair<T, U>;
#include <iostream>
template <typename T, typename U>
struct MyPair {
	T first{};
	U second{};
};
template <typename T, typename U>
MyPair(T, U) -> MyPair<T, U>;
int main() {
	MyPair<int, int> p1{ 1, 2 };
	MyPair p2{ 1, 2 };
	std::cout << p1.first << ", " << p1.second << std::endl;
	std::cout << p2.first << ", " << p2.second << std::endl;
	return 0;
}

类模板参数推导(CTAD)通过允许编译器从构造函数参数中推导出模板参数,简化了类模板的实例化过程。在引入CTAD之前,开发者在实例化时必须明确指定模板参数。然而,通过CTAD,这种明确的指定变得不再必要,从而使代码更易读、易维护。

1. 基本示例:简单栈类
#include <iostream>
#include <vector>
#include <cassert>

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

public:
    void push(T const& elem) {
        elems.push_back(elem);
    }

    void pop() {
        assert(!elems.empty());
        elems.pop_back();
    }

    T const& top() const {
        assert(!elems.empty());
        return elems.back();
    }

    bool empty() const {
        return elems.empty();
    }

    // 默认构造函数
    Stack() = default;

    // 推导引导需要的构造函数
    Stack(T const& elem) {
        elems.push_back(elem);
    }
};
// 提供一个推导指引
template<typename T>
Stack(T) -> Stack<T>;

int main() {
    Stack myStack{ 1 };  // 利用CTAD进行自动类型推导为 Stack<int>
    myStack.push(2);
    myStack.push(3);

    while (!myStack.empty()) {
        std::cout << myStack.top() << std::endl;
        myStack.pop();
    }
    return 0;
}
2. 复合数据类型示例:栈中的对组合型

假设我们有一个栈,其中每个元素是一个std::pair,我们希望使用参数推导:

 #include <iostream>
#include <vector>
#include <cassert>
#include <utility>

// 定义 Stack 类模板
template<typename T>
class Stack {
private:
    std::vector<T> elems;

public:
    Stack() = default; // 明确提供默认构造函数

    void push(T const& elem) {
        elems.push_back(elem);
    }

    void pop() {
        assert(!elems.empty());
        elems.pop_back();
    }

    T const& top() const {
        assert(!elems.empty());
        return elems.back();
    }

    bool empty() const {
        return elems.empty();
    }
};

// 自定义推导指南
Stack()->Stack<std::pair<int, double>>; // 这个指南假设默认类型是 std::pair<int, double>

int main() {
    Stack pairStack; // 这利用了手动设置的推导指南,自动推导为 Stack<std::pair<int, double>>
    pairStack.push({ 1, 4.5 });
    pairStack.push({ 2, 3.7 });
    pairStack.push({ 3, 2.2 });

    while (!pairStack.empty()) {
        auto [first, second] = pairStack.top(); // 使用结构化绑定
        std::cout << first << ", " << second << std::endl;
        pairStack.pop();
    }

    return 0;
}
3. 自定义类类型示例

假设我们有一个自定义类型,并希望将其实例存入栈中:

  自定义推导指南
//Stack()->Stack<std::pair<int, double>>; // 这个指南假设默认类型是 std::pair<int, double>





#include <iostream>
#include <vector>
#include <cassert>

class MyClass {
public:
    int data;
    MyClass(int value) : data(value) {}
};

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

public:
    void push(T const& elem) {
        elems.push_back(elem);
    }

    void pop() {
        assert(!elems.empty());
        elems.pop_back();
    }

    T const& top() const {
        assert(!elems.empty());
        return elems.back();
    }

    bool empty() const {
        return elems.empty();
    }
};
Stack()->Stack<MyClass>; // CTAD 自动类型推导
int main() {
    Stack myClassStack; // 自动推导为 Stack<MyClass>
    myClassStack.push(MyClass(10));
    myClassStack.push(MyClass(20));

    while (!myClassStack.empty()) {
        std::cout << myClassStack.top().data << std::endl;
        myClassStack.pop();
    }

    return 0;
}

9.3 编写 自动类型推导 CTAD

9.3.1 array 和 greater
#include <algorithm>
#include <array>
#include <functional>
#include <iostream>
#include <string_view>
#include <type_traits>
using namespace std;

int main() {
    array arr = { "lion"sv, "direwolf"sv, "stag"sv, "dragon"sv };
    static_assert(is_same_v<decltype(arr), array<string_view, 4>>);
    sort(arr.begin(), arr.end(), greater{});
    cout << arr.size() << ": ";
    for (const auto& e : arr) {
        cout << e << " ";
    }
    cout << "\n";
}

编译与运行示例:

C:\Temp>cl /EHsc /nologo /W4 /std:c++17 arr.cpp && arr
arr.cpp
4: stag lion dragon direwolf

这个示例展示了几个有趣的特性。首先,std::array 的 CTAD 推导出了其元素类型以及大小。其次,CTAD 能与默认模板参数一起使用;greater{} 构造了一个类型为 greater<void> 的对象,因为它被声明为 template <typename T = void> struct greater;


这行代码:

static_assert(is_same_v<decltype(arr), array<string_view, 4>>);

是在编译时进行类型验证的断言。让我们逐部分理解这段代码:

  1. decltype(arr): 这是 C++ 中的一种操作符,用于获取变量、表达式或实体的类型。在此例中,它获取变量 arr 的类型。

  2. array<string_view, 4>: 这是一个 std::array 类型,其中元素类型为 std::string_view,并且数组大小为 4。

  3. is_same_v<T, U>: 这是标准库中的一种类型特征(Type Trait),得自 std::is_sameis_same_v<T, U> 在编译时计算 TU 是否为相同类型,如果相同,值为 true,否则为 false

  4. static_assert: 这是一种在编译时进行断言检查的语句。如果其条件为 false,编译器会生成一条错误信息,并停止编译。

综合来看,这行代码是确保在编译时 arr 的类型确实是 std::array<string_view, 4>。如果类型不匹配,程序不会编译成功。这对于验证在模板或泛型编程中类型推导是否正确非常有用。在这个例子中,因为 arr 是用 4 个 std::string_view 初始化的,所以其类型正是 array<string_view, 4>


std::greater 是 C++ 标准库中的一个函数对象类,用于进行大于比较操作。它通常用于排序和比较算法中,例如 std::sortstd::set

构造 std::greater 对象

在 C++ 中,std::greater 是一个模板类,通常被声明为 template <typename T> struct greater;。但在 C++14 之后,std::greater<void> 也被引入,以支持类型擦除和更灵活的比较操作。

当你看到 greater{} 这样的构造形式,解释如下:

  1. 默认模板参数template <typename T = void> 的含义是 T 有一个默认值 void。这意味着如果你没有在构造 std::greater 对象时指定类型参数,它将使用 void 作为参数类型。

  2. 类型为 void 的特殊化:当 T = void 时,这种特殊化让 std::greater<void> 可以接受任意可以进行 > 比较操作的参数。它不依赖于静态类型检查(即在编译期检查),而是在运行时进行动态类型检查(即在运行期检查),这在模板参数推导或泛型编程中能提供更大的灵活性。

  3. 对象实例化greater{} 语法表示使用默认构造函数创建一个 std::greater<void> 对象。这种构造方式,以 {} 花括号形式表示,通常用于 C++11 之后的统一初始化语法。

在使用 std::greater<void> 时,函数对象会以参数调用它的 operator(),这会检查这些参数是否可以使用 > 进行比较。例如,std::greater<void>()(a, b) 将比较 a > b,并返回一个布尔值。

这种类型擦除方式的主要优势在于提供了一种通用的二元比较操作符,支持任意类型实例,而不是必须局限于特定的静态类型 T。这样,当使用 STL 算法时,你可以在不指定精确类型的情况下利用 std::greater 进行比较。


9.3.2 辅助类型推导

在C++17中引入了类模板参数推导(Class Template Argument Deduction,简称CTAD),它允许编译器根据构造函数的参数来自动推导模板类的类型参数。在你提供的代码示例中,MyPair是一个模板结构体,它有两个构造函数。

代码如下:

#include <type_traits>

template <typename A, typename B>
struct MyPair {
    MyPair() { }
    MyPair(const A&, const B&) { }
};

int main() {
    MyPair mp{11, 22};
    MyPair mp{}; //会报错
    static_assert(std::is_same_v<decltype(mp), MyPair<int, int>>);
}

在这个例子中,MyPair mp{11, 22}; 这行代码使用了MyPair的第二个构造函数,并且传入了两个int类型的参数。编译器看到构造函数的参数后,会运行模板参数推导,根据参数类型来推断模板参数。因为构造函数的签名是(const A&, const B&),并且参数是int类型,所以编译器会推断出AB都是int类型,然后使用这些类型参数来实例化MyPair类和构造函数。

然而,如果你尝试创建一个没有参数的MyPair实例,比如MyPair{};,编译器会报错。这是因为编译器会尝试根据空的构造函数参数来推导AB的类型,但是因为没有提供任何参数,也没有默认的模板参数,所以编译器无法猜测你想要实例化的是MyPair<int, int>还是MyPair<Starship, Captain>

简而言之,CTAD允许编译器根据构造函数的参数来自动推断模板参数的类型,但是如果构造函数没有参数,编译器就无法进行推导,因为没有足够的信息来确定模板参数的类型。


要实现类型推断,你需要确保在没有参数情况下也能推断出模板参数的类型。可以通过提供默认模板参数或添加具有明确类型信息的辅助函数或者构造函数来实现这一点。以下是通过提供默认模板参数来实现类型推断的示例:

#include <type_traits>

// Define a template struct MyPair with default template parameters
template <typename A = int, typename B = int> 
struct MyPair {
    // Default constructor
    MyPair() { }
    
    // Parameterized constructor
    MyPair(const A&, const B&) { }
};

int main() {
    // Create an instance of MyPair using CTAD with constructor arguments
    MyPair mp1{11, 22};
    static_assert(std::is_same_v<decltype(mp1), MyPair<int, int>>);
    
    // Create an instance of MyPair using CTAD without arguments
    MyPair mp2{};
    static_assert(std::is_same_v<decltype(mp2), MyPair<int, int>>);
}

说明

  • 默认模板参数: 通过为模板参数 AB 提供默认类型 int,如果没有提供任何构造函数参数,编译器仍然能够推断出类型。这使得在使用默认构造函数时 MyPair 会被实例化为 MyPair<int, int>

这允许你创建一个 MyPair 的实例而不需要提供任何显式模板参数或构造函数参数,从而支持类型推断。这样,使用 MyPair{} 将不再导致编译错误,因为编译器可以使用默认的模板参数类型 int


通过增加辅助函数来实现类型推导是一个常见的技巧,称为“工厂函数”或“辅助函数”。这个助手函数的作用是显式地使用一个函数模板来推导模板参数,然后返回适当的类型实例。这种方法在C++17之前是一个常见的模式,因为那时候还没有CTAD(Class Template Argument Deduction)。

以下是如何为你的MyPair结构体增加一个辅助函数来实现类型推导的示例:

#include <type_traits>
#include <utility>

// 原来的类型模板定义
template <typename A = int, typename B = int>
struct MyPair {
    // 默认构造函数
    MyPair() { }
    
    // 带参数的构造函数
    MyPair(const A&, const B&) { }
};

// 辅助函数,用于类型推导
template <typename A, typename B>
MyPair<A, B> makeMyPair(const A& a, const B& b) {
    return MyPair<A, B>(a, b);
}

int main() {
    // 使用辅助函数进行推导
    auto mp1 = makeMyPair(11, 22);
    static_assert(std::is_same_v<decltype(mp1), MyPair<int, int>>);

    // 手动指定类型的使用情况
    MyPair<int, int> mp2;  // 由于没有参数传入,因此需要明确指定类型或接受默认类型

    return 0;
}

解释:

  • makeMyPair 函数:这是一个模板函数,用来从传入的参数推导出类型。它返回一个MyPair实例,通过传入的参数ab自动推导出类型AB

  • decltypestatic_assert:这些用来验证类型推导是否正确。

这样,通过makeMyPair函数来构造MyPair对象时,编译器将根据传入的参数类型推导出适当的模板参数。工厂函数模式不仅可以用于类型推导,还能简化复杂对象的构造过程。


9.3.3 CTAD的例子

当然,这里是对给定内容进行翻译和排版:

在一般情况下,当类模板拥有构造函数且其签名包含所有的类模板参数时(如上述的 MyPair),类模板参数推断(CTAD)可以自动工作。然而,有时构造函数本身也被模板化,这就破坏了 CTAD 赖以工作的联系。在这种情况下,类模板的作者可以提供“推断指南”,告诉编译器如何从构造函数参数推断出类模板参数。

#include <iterator>
#include <type_traits>

// 定义模板类 MyVec
template <typename T>
struct MyVec {
    // 模板化构造函数
    template <typename Iter>
    MyVec(Iter, Iter) { }
};

// 推断指南
template <typename Iter>
MyVec(Iter, Iter) -> MyVec<typename std::iterator_traits<Iter>::value_type>;

// 定义模板类 MyAdvancedPair
template <typename A, typename B>
struct MyAdvancedPair {
    // 模板化构造函数
    template <typename T, typename U>
    MyAdvancedPair(T&&, U&&) { }
};

// 推断指南
template <typename X, typename Y>
MyAdvancedPair(X, Y) -> MyAdvancedPair<X, Y>;

// 主函数
int main() {
    int *ptr = nullptr;
    
    // 使用指针创建 MyVec 实例
    MyVec v(ptr, ptr);
    static_assert(std::is_same_v<decltype(v), MyVec<int>>);
    
    // 使用整型和字符串字面量创建 MyAdvancedPair 实例
    MyAdvancedPair adv(1729, "taxicab");
    static_assert(std::is_same_v<decltype(adv), MyAdvancedPair<int, const char *>>);
}
说明
  • MyVec 的推断指南: MyVec 类似于 std::vector,其模板参数是一个元素类型 T,但其可由一个迭代器类型 Iter 构建。调用范围构造函数提供了我们需要的类型信息,但编译器无法自动识别 IterT 之间的关系。推断指南帮助解决了这个问题。通过推断指南 template <typename Iter> MyVec(Iter, Iter) -> MyVec<typename std::iterator_traits<Iter>::value_type>;, 告诉编译器在对 MyVec 进行 CTAD 时,尝试对 MyVec(Iter, Iter) 的签名进行模板参数推断。如果成功,要构造的类型是 MyVec<typename std::iterator_traits<Iter>::value_type>。这实际上是通过解引用迭代器类型来获取我们所需的元素类型。

  • MyAdvancedPair 的推断指南: 使用完全转发的构造方式创建一个 MyAdvancedPair 实例,通过推断指南确保生成的类型为 MyAdvancedPair<int, const char *>, 匹配传入参数的类型。

在这两个例子中,推断指南有助于在特定的构造函数上下文中正确推断模板参数类型。这在标准库中是很常见的,尤其是在涉及迭代器和完美转发的场景中。


解释下面的代码段

template <typename Iter> MyVec(Iter, Iter) -> MyVec<typename std::iterator_traits<Iter>::value_type>;

这行代码定义了一个用于类模板 MyVec 的“推断指南”(deduction guide),用来帮助编译器在模板类实例化时进行正确的类型推断。让我们逐步分析这行代码:

分解与分析
  1. template <typename Iter>

    • 这是推断指南本身的模板头,表示这个推断指南是针对任意类型的迭代器 Iter 定义的。
  2. MyVec(Iter, Iter)

    • 这一部分指定了构造函数的参数形式,即当 MyVec 的构造函数被调用且传入的参数是两个类型为 Iter 的迭代器时,启用此推断指南。
  3. -> MyVec<typename std::iterator_traits<Iter>::value_type>

    • 这是推断指南的返回部分,告诉编译器应将 MyVec(Iter, Iter) 构造函数调用实例化为 MyVec,其模板参数为 typename std::iterator_traits<Iter>::value_type
    • std::iterator_traits<Iter> 是一个模板类,用于获取迭代器类型相关的信息。value_type 是该迭代器的元素类型,也就是迭代器所指向的对象的类型。
举例说明

假设你有一个指向 int 的指针 int* ptr;,并且你使用 MyVec 的构造函数 MyVec(ptr, ptr);

  • 迭代器类型 Iter: 在这个例子中,Iter 将是 int*
  • 通过推断指南进行推导:
    • std::iterator_traits<int*>::value_type 被解析为 int。这是因为指针类型的 value_type 通常是它所指向的对象类型。
  • 因此,MyVec(ptr, ptr) 被推导为 MyVec<int>

这个推断指南帮助编译器了解,如何从构造函数的参数(两个迭代器)中推导出模板参数,从而正确实例化 MyVec 的参数化类型。

9.3.4 推导的非侵入性

当然,为了更好地理解类模板参数推导(CTAD)和推导指南的工作方式,我们可以看一个简单的例子:

假设我们有一个模板类 MyAdvancedPair,它类似于 std::pair,用于存储两个值:

template <typename T, typename U>
struct MyAdvancedPair {
    T first;
    U second;

    // 构造函数
    MyAdvancedPair(T f, U s) : first(f), second(s) {}

    // 完美转发的构造函数
    template <typename X, typename Y>
    MyAdvancedPair(X&& f, Y&& s) 
        : first(std::forward<X>(f)), second(std::forward<Y>(s)) {}
};

// 推导指南
template <typename X, typename Y> 
MyAdvancedPair(X, Y) -> MyAdvancedPair<X, Y>;

在没有推导指南之前,我们必须显式指定模板参数,例如:

MyAdvancedPair<int, double> p1(42, 3.14);

但是在添加了推导指南之后,我们可以利用 CTAD 让编译器自动推导类型:

MyAdvancedPair p2(42, 3.14);  // 自动推导为 MyAdvancedPair<int, double>

在这个例子中,推导指南 MyAdvancedPair(X, Y) -> MyAdvancedPair<X, Y>; 告诉编译器:当提供给 MyAdvancedPair 的参数是 (X, Y) 类型时,将目标类型推导为 MyAdvancedPair<X, Y>。编译器会查看构造函数,并按照最初指定的构造方式进行参数衰变和类型推导。

这样,通过使用推导指南,我们可以简化代码的编写,而不再需要显式地提供类型参数。正如文本中提到的,这种推导的非侵入性使得我们可以在不影响现有代码的情况下,添加推导能力。


9.3.5 强制规则

在某些罕见的情况下,你可能希望通过推导指南来拒绝某些代码。以下是 std::array 的实现方法:

#include <stddef.h>
#include <type_traits>

template <typename T, size_t N> struct MyArray {
    T m_array[N];
};

template <typename First, typename... Rest> struct EnforceSame {
    static_assert(std::conjunction_v<std::is_same<First, Rest>...>);
    using type = First;
};

template <typename First, typename... Rest> 
MyArray(First, Rest...) -> MyArray<typename EnforceSame<First, Rest...>::type, 1 + sizeof...(Rest)>;

int main() {
    MyArray a = { 11, 22, 33 };
    static_assert(std::is_same_v<decltype(a), MyArray<int, 3>>);
}

编译和结果显示:

C:\Temp>cl /EHsc /nologo /W4 /std:c++17 enforce.cpp
enforce.cpp
C:\Temp>

std::array 类似,MyArray 是一个没有实际构造函数的聚合体,但通过类模板推导指引(CTAD),这种模板类仍然可以工作。MyArray 的推导指南执行了 MyArray(First, Rest...) 的模板参数推导,强制要求所有类型相同,并通过参数数量确定数组的大小。

类似的技术可以用于使某些构造函数或者所有构造函数的 CTAD 变得完全不符合格式。不过,STL 本身并不需要显式地做到这一点(只有两个类不希望使用 CTAD:unique_ptrshared_ptr)。C++17 支持数组的 unique_ptrshared_ptr,但由于 new Tnew T[N] 都返回 T *,所以从原始指针构造 unique_ptrshared_ptr 时缺乏足够的信息来安全地推导类型。巧合的是,由于 unique_ptr 支持高级指针(fancy pointers),以及 shared_ptr 支持类型抹消(type erasure),两者都改变了构造函数的签名,阻止了 CTAD 的工作。)


解释代码段

template <typename First, typename... Rest> MyArray(First, Rest...) -> MyArray<typename EnforceSame<First, Rest...>::type, 1 + sizeof...(Rest)>; 解释

这行代码是一个类模板推导指引(CTAD),用于 MyArray 的实例化。具体来说,它帮助编译器在没有明确类型信息的情况下推导出 MyArray 的模板参数。让我们逐步解释这行代码:

  1. 模板参数包template <typename First, typename... Rest> 这部分声明一个模板参数包,First 表示第一个参数的类型,Rest... 表示零个或多个其他参数的类型。这意味着这个推导指引可以接受至少一个参数。

  2. 构造函数样式MyArray(First, Rest...) 这部分类似于一个没有实现的函数声明,描述了一个传入的参数模式。这里的 First, Rest... 表示可以接受多个参数,其中第一个参数的类型为 First,其余参数的类型存放在参数包 Rest 中。

  3. 推导结果

    • MyArray<typename EnforceSame<First, Rest...>::type, 1 + sizeof...(Rest)> 是推导的目标类型:
      • typename EnforceSame<First, Rest...>::type 是用来确保所有参数类型相同的部分。EnforceSame 是一个辅助模板,它使用 static_assert 确保所有类型匹配。如果类型不匹配,编译时会失败。
      • 1 + sizeof...(Rest) 计算了传入参数的数量,因为 sizeof...(Rest) 返回参数包 Rest 的元素数目,加上 1 是为了包括 First

简而言之,这个推导指引使 MyArray 的模板参数可以自动推导:

  • 如果全部参数类型相同,那么 MyArray 的类型为 MyArray<T, N>,其中 T 是参数的公共类型,N 是参数的数量。
  • 如果参数类型不同,编译器会因为 EnforceSamestatic_assert 而抛出编译错误,拒绝该代码。

解释代码段

static_assert(std::conjunction_v<std::is_same<First, Rest>...>); 解释这个

static_assert(std::conjunction_v<std::is_same<First, Rest>...>); 是一种编译时断言,用于确保给定类型是相同的。在C++中,static_assert用于在编译时检测条件是否为真,如果为假,则会产生编译错误。让我们详细分析这个表达式:

1. std::is_same

  • std::is_same<T, U> 是一个类型特征,用于检查两个类型 TU 是否相同。
  • 如果 TU 相同,则 std::is_same<T, U>::valuetrue,否则为 false

2. std::conjunction

  • std::conjunction 是C++17中引入的一种模板,用于对一系列布尔类型表达式进行逻辑与操作。
  • 如果所有模板参数都为 true,则 std::conjunction 的结果为 true,否则为 false

3. ... (折叠表达式)

  • ... 是C++中的折叠表达式的一种,用于将参数包展开。
  • 在这种情况下,std::is_same<First, Rest>... 展开为一系列的类型比较,如 std::is_same<First, Rest1>, std::is_same<First, Rest2>, ...

整体作用

  • std::conjunction_v<std::is_same<First, Rest>...> 会在所有的 Rest 类型中,一一与 First 进行类型比较。如果所有类型 Rest1, Rest2, ... 都与 First 相同,则 std::conjunction_v<...> 的结果为 true

  • static_assert 用于在编译时检查这个逻辑表达。如果类型不完全相同,则会导致编译错误,提供有助于找出代码结构性问题的信息。

示例:

假设我们检查类型参数包 Rest 是否所有类型都与 First 相同:

template<typename First, typename... Rest>
void checkTypes() {
    static_assert(std::conjunction_v<std::is_same<First, Rest>...>, "All types must be the same as First");
}

// 用法
checkTypes<int, int, int>(); // 通过
checkTypes<int, double, int>(); // 编译错误,double 与 int 不同

在以上示例中,checkTypes<int, int, int>() 将通过编译,因为所有类型参数都是 int。然而,调用 checkTypes<int, double, int>() 会导致编译错误,因为 doubleint 不同。


template<typename T,size t size>struct arrayT arr[size];};template<class Tu,class...Tp>array(Tu,Tp...)->array<std:enable_if_t(std::is_same_v<Tu,Tp>&&...)Tu>,sizeof...(Tp)+1>; 和这个有什么不同

这段代码也是一个类模板推导指引(CTAD),它与前面的代码有相似之处,但是在类型推导和处理方面有一些区别。让我们分析这段代码:

  1. 类模板声明

    template<typename T, size_t size>
    struct array {
        T arr[size];
    };
    

    这定义了一个模板类 array,其中 T 是数组元素的类型,size 是数组的大小。它有一个成员 arr,这是一个固定大小的 C++ 原生数组。

  2. 模板推导指引

    template<class Tu, class... Tp>
    array(Tu, Tp...) -> array<
        std::enable_if_t<(std::is_same_v<Tu, Tp> && ...), Tu>,
        sizeof...(Tp) + 1
    >;
    

    这部分定义了一个推导指引,供编译器在缺乏显式类型时使用。

    • TuTp... 分别是第一个参数和其余参数的类型。class Tu, class... Tp 使用了类类型来声明这些模板参数。

    • array(Tu, Tp...) 指明这条指引适用于构造 array 的参数列表,其中第一个参数有类型 Tu,而其余参数有类型存储在 Tp... 中。

    • std::enable_if_t<(std::is_same_v<Tu, Tp> && ...), Tu>

      • std::is_same_v<Tu, Tp> 是一种用来检查 Tu 和每个 Tp 是否相同的变体。
      • && ... 是 C++17 中的折叠表达式,用于保证所有 Tp 的类型都与 Tu 相同。
      • std::enable_if_t<condition, Tu> 只有在 conditiontrue 时才能成功。不满足时,这个类型表达式无效,从而导致类型推导失败。
    • sizeof...(Tp) + 1 计算了模板参数的数量,因为 sizeof...(Tp) 返回参数包 Tp 中的参数个数,加上 1 是包括 Tu 的数量。

区别总结:
  1. 辅助类型检查机制:前面的代码示例使用了 EnforceSame 这个假设存在的模板,而这段代码则使用了标准库中的 std::enable_if_tstd::is_same_v 来保证类型一致性。

  2. 类型表达:前一个代码示例没有显式使用 enable_if,而这个例子中 enable_if 用于确保类型一致性和推导指引的条件有效性。

  3. 推导指引格式:尽管两者都在本质上检查了所有传入类型是否相同并计算参数数量,但它们使用了不同的工具和方式来实现。

这两者的核心思想相同:为了自动推导出相同类型元素的数组并计算数组长度。然而,它们涉及的实现机制和选择的语言特性各不相同。

9.3.6 模板聚合

是指一个满足某些条件的类或结构体,它是参数化的并且可以像普通聚合类型一样使用。在C++中,这类聚合不能由用户提供显式构造函数,也不能包含私有或受保护的非静态数据成员、虚函数、或者 virtual、private 或 protected 的基类。

例如,ValueWithComment 是一个模板聚合结构体,它有一个参数化类型 T 的成员 value 和一个 std::string 类型的成员 comment。这个结构体可以像一般的类模板那样声明对象,并以聚合的方式使用。

从C++17开始,可以为这种聚合类模板定义推导策略(deduction guide)。推导策略允许在没有构造函数的情况下,通过推导实现初始化。例如,通过定义推导策略可以将 ValueWithComment{"hello", "initial value"} 推导为 ValueWithComment<std::string>

标准库中的 std::array 也是一个类似的聚合类型,它参数化为元素类型和大小,并在C++17中定义了推导策略。

通过这个方法,模板聚合类可以在初始化时进行类型参数的自动推导,简化了使用过程。


当然!下面是一个关于如何使用模板聚合和C++17推导策略的例子。

首先,我们定义一个模板聚合结构体:

#include <string>
#include <iostream>

template<typename T>
struct ValueWithComment {
    T value;
    std::string comment;
};

// C++17 deduction guide
ValueWithComment(const char*, const char*) -> ValueWithComment<std::string>;

int main() {
    // Using the template with explicit type
    ValueWithComment<int> vc1;
    vc1.value = 42;
    vc1.comment = "initial value";

    std::cout << "vc1.value: " << vc1.value << ", vc1.comment: " << vc1.comment << std::endl;

    // Using the deduction guide
    ValueWithComment vc2 = {"hello", "initial value"};
    std::cout << "vc2.value: " << vc2.value << ", vc2.comment: " << vc2.comment << std::endl;

    return 0;
}

在这个例子中:

  1. 我们定义了一个模板结构体 ValueWithComment,它有一个类型 T 的成员 value 和一个 std::string 类型的成员 comment

  2. 我们显式地为 ValueWithComment 定义了一个推导策略,ValueWithComment(const char*, const char*) -> ValueWithComment<std::string>;。这个推导策略告诉编译器,当看到 ValueWithComment 初始化列表包含两个 const char* 时,它应该推导出类型为 ValueWithComment<std::string>

  3. main 函数中,vc1 是使用模板时显式指定类型参数 int 的一个示例。

  4. vc2 显示了推导策略的作用,它初始化两个 const char*,然后推导出类型为 ValueWithComment<std::string>,因此 vc2.value 的类型就是 std::string

这个例子展示了如何利用C++17的推导策略来简化模板聚合类型的使用,无需显式地提供模板参数。

进一步解释


构造函数的函数名称不要使用 模板参数 stack<T>

在模板类中,当我们只需要类的名字,而不需要具体类型时,可以只用类名而不加上模板参数。通常这种情况出现在构造函数和析构函数的声明中,因为它们只需要表示类本身,而不需要涉及到模板参数的具体实例化。

示例说明

假设我们有一个模板类 Stack,我们想实现一个复制构造函数和析构函数。在这种情况下,函数的定义不需要具体的模板参数类型,只需要知道它们属于 Stack 类即可。

template<typename T>
class Stack {
public:
    Stack();                  // 构造函数
    Stack(Stack const&);      // 复制构造函数
    ~Stack();                 // 析构函数
    
    // 其他成员函数和变量
};
在实现时

在实现这些函数时,虽然它们是模板类的一部分,但它们只需要代表所属的类 Stack,而不是某个具体的 Stack<T> 类型。例如:

template<typename T>
Stack<T>::Stack() {
    // 构造函数的实现
}

template<typename T>
Stack<T>::Stack(Stack const& other) {
    // 复制构造函数的实现
}

template<typename T>
Stack<T>::~Stack() {
    // 析构函数的实现
}

结论

在上面的实现中,尽管 Stack 是一个模板类,复制构造函数的参数中的 Stack 和析构函数的实现中只使用类名都是可以的。在这种情形下,模板参数信息已经隐含在类的定义中,不需要在构造函数和析构函数的参数中再次指定。在类的内部,Stack 已经代表了 Stack<T> 类型。

模板必须在全局或命名空间作用域,或者类的范围内定义

模板必须在全局或命名空间作用域,或者类的范围内定义,这是由C++语言的设计所决定的,原因主要在于模板的唯一性和可重用性,以及编译器处理模板的方式。

原因分析

  1. 统一的实例化机制

    • 模板是一种通用类型,需要在不同的地方分别实例化,并且实例化的细节是在使用模板时根据传递的类型参数来决定的。
    • 如果模板定义在局部作用域,比如函数内部或一个代码块内,编译器在不同地方实例化时容易出现作用域冲突或不一致的问题。
  2. 作用域限制

    • 局部作用域的模板定义会使模板在函数外部不可见,从而限制了模板的重用性。
    • 全局或命名空间作用域使得模板在整个程序中都可见,这样同一模板可以在不同的上下文中被实例化和使用。
  3. 编译器的实现复杂性

    • 模板的实例化是在编译时完成的,编译器需要在模板实例化时能够查找到模板的定义。
    • 如果模板在局部作用域内,编译器在访问和解析这些模板时会增加实现的复杂性,因为这需要更细致的作用域管理。
  4. 语言设计和规范

    • C++的设计者希望模板提供一种简单而一致的方式来实现泛型编程。因此,模板被设计为只能在一定的范围内定义,这也符合大多数程序设计的需要。
  5. 可维护性

    • 定义在全局、命名空间或类中的模板更容易被管理和理解,这样可以避免在复杂项目中由于模板的局部定义而引起的混乱。

综上所述,将模板限制在全局、命名空间或类作用域之外,有助于保持语言的一致性和易于理解,同时使编译器在处理模板时更加高效。

在C++中,使用类模板的成员函数时,需要遵循特定的语法规则,以确保编译器正确识别和实例化模板。这是因为模板的实现涉及到将通用类型和行为参数化,使得同一份代码能够为不同的数据类型生成专用的版本。当我们实现类模板的成员函数时,需要在实现处指明这确实是一个模板类,并提供相应的类型参数。这通常是通过template关键字和相应的模板参数列表实现的。

为什么需要指明模板类?

  1. 模板参数的作用

    • 类模板的声明与一般类不同,因为它是以类型参数化的,所以需要明确它是一个模板,并提供这些类型参数,否则编译器无法识别某个类或函数与哪个模板关联。
  2. 作用域解析

    • 当在类外实现成员函数时,编译器需要知道这个函数属于哪个模板类。因此,必须包括模板参数列表以进行作用域解析。例如:
      template<typename T>
      class Stack {
          void push(T const& elem);
      };
      
      template<typename T>
      void Stack<T>::push(T const& elem) {
          elems.push_back(elem);
      }
      
    • 这里,<T>表明我们正在实现模板类Stack对应的某个特定类型T的成员函数。
  3. 使得代码复用得以实现

    • 通过为类模板提供类型参数,编译器可以为该模板生成不同类型版本以进行实例化。这在编写泛型代码时非常有用,因为它减少重复代码和类型特定实现所需的工作。

作用和好处

  • 类型安全:模板让我们能够编写类型安全的代码,同时在需要时适应不同的数据类型。
  • 代码复用:模板使得类把实现和接口解耦,从而提高了复用性。
  • 灵活性:使用模板类可以在不改变类代码的情况下处理各种数据类型。

反之,如果没有标明模板参数,编译器会将其视为普通类的成员函数实现,即没有进行任何参数化的普通类,而这通常是一个错误的语法。

总的来说,通过指明模板类并明确其类型参数,可以帮助编译器正确定位关联的模板,并为每种使用实例化正确的代码。

这段文字讨论了在使用标准库中的std::stack时,如何避免因异常导致数据丢失的问题。

在使用栈的数据结构时,如果同时执行“获取并移除元素”的操作,可能会产生一个问题:当你从栈中弹出一个元素(即“移除”操作)后,如果在将这个元素的副本返回给调用者的过程中出现了异常(例如,由于内存不足导致的复制失败),这个元素就会丢失,因为它已经从栈中移除了,但复制还没有成功完成。这意味着你失去了这个特定的数据信息,却没有办法复原。

为了避免这个问题,std::stack接口将这些操作分为两步:首先,通过top()来访问栈顶元素,这样元素还保留在栈中;其次,通过pop()明确地将元素移除。这样设计的好处在于,如果在复制数据的过程中出现异常,数据不会立即丢失,因为它仍然在栈上。这为开发者提供了一个回旋的余地,比如释放一些内存然后重试该操作。

这种设计考虑确保了数据处理的安全性,尤其在异常处理和资源受限(如内存不足)的情况下更为重要。

但若特化类模板,则必须特 化所有成员函数。虽然可以特化类模板的单个成员函数,但若这样做了,就不能再特化该特化成员 所属的整个类模板实例。

当然。下面是一个关于类模板和成员函数特化的简单例子。

#include <iostream>

// 通用类模板
template <typename T>
class MyClass {
public:
    void func() {
        std::cout << "General template version for func()" << std::endl;
    }
};

// 特化成员函数
template <>
void MyClass<int>::func() {
    std::cout << "Specialized template version for func() with int" << std::endl;
}

// 如果尝试对整个类进行特化
/*
template <>
class MyClass<int> {
public:
    void func() {
        std::cout << "Complete specialization for MyClass<int>" << std::endl;
    }
};
*/

int main() {
    MyClass<double> obj1;
    obj1.func();  //输出: General template version for func()

    MyClass<int> obj2;
    obj2.func();  //输出: Specialized template version for func() with int

    return 0;
}

在这个例子中:

  • MyClass<T>是一个通用的类模板。
  • 我们为 func() 做了 int 类型的特化,因此 MyClass<int>func() 会调用特化版本。
  • MyClass<double> 使用的是通用模板的版本。

注意注释掉的部分,尝试为整个 MyClass<int> 特化,这会导致一个编译问题,因为 func() 已经被特化了。如果你尝试取消注释并进行编译,编译器会报错,因为它不知道该选择哪个特化版本。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

丁金金

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

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

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

打赏作者

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

抵扣说明:

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

余额充值