C++模板编程与泛型编程之函数模板

文章目录

  • 函数模板(第一部分)
      • 定义函数模板
      • 使用函数模板
            • 样例
      • 两阶段翻译 Two-Phase Translation
        • 模板的编译和链接问题
      • 多模板参数
        • 引入额外模板参数作为返回值类型
        • 让编译器自己找出返回值类型
        • 将返回值声明为两个模板参数的公共类型
            • 样例
      • 默认模板参数
            • 样例
      • 重载函数模板
      • 模板函数特化
      • 非类型模板参数
  • C++泛型编程类模板,变量模板,别名模板,CSATD(第二部分)
      • **类模板声明、实现与使用**
          • 声明:
          • 实现:
          • 使用:
        • 注意
      • 例子
    • 类模板的参数推导
        • **Deduction Guides**
      • 例子
    • 类模板实参推导
    • CTAD是如何工作的?
      • 第一步:
      • 第二步:
    • **类模板的泛化; 类模板的偏特化; 类模板的全特化; 静态成员变量的全特化; 普通成员变量的全特化**
    • 类模板的默认参数
    • 类型别名
    • 非类型模板参数的使用
    • 成员函数模板;模板类虚函数;模板虚函数;拷贝构造函数;拷贝构造函数模板;赋值运算符;赋值运算符模板
    • 成员函数的泛化版本;成员函数的特化版本;类外定义的全特化版本; 函数模板的嵌套;类模板的嵌套
    • 类模板显式实例化定义/声明
      • 类模版的隐式实例化
      • 2. 显式实例化声明、定义
        • 3. 显式实例化的用途
        • 注意
            • 例子
      • 模板的编译和链接问题
    • 变量模板
      • 变量模板的定义,泛化,全特化,偏特化
      • 变量模板在C++14 之前的实现
        • 方式一
        • 方式二
      • C++14 的变量模板
        • 变量模板的作用
      • C++17 类型特性后缀
    • 变量模板的默认模板参数; 变量模板的非类型模板参数
    • 别名模板
    • 成员变量模板
  • 类模板的友元(第三部分)
      • 让类模板的某个实例(具体的类)成为友元类
      • 让类模板成为友元类模板
      • 让类型模板参数成为友元类
        • 增加 friend class CF;则将整个CF类作为友元类
      • 让函数模板的某个实例成为友元函数
      • 让函数模板func成为类模板Men的友元函数模板
    • 在类模板中定义友元函数
  • 可变参模板指南(第四部分)
      • 可变参函数模板
        • 基本外观和介绍
        • 展开参数包(获取参数包的值)
          • 错误的演示
          • 正确的演示
            • 采取递归方式
            • 采取[逗号表达式](https://so.csdn.net/so/search?q=逗号表达式&spm=1001.2101.3001.7020)
            • 为什么需要逗号表达式
            • 其他方式
      • 应用
        • 1.求最大值(可接受多个参数)
        • 2. 用可变参数模板函数模仿printf的功能
        • 3. 使用tuple转化并分解参数包
    • 获取可变参函数模板中的值
      • 可变参类模板
        • 基本外观和介绍
        • **三段式** :
        • 两段式:
      • 执行结果和原理分析
      • 如何获取参数包中的值
    • 组合+递归形式 实现
    • 泛型的应用
      • 4 可变参数模版实现泛化的delegate
      • 5 总结
    • 折叠表达式
        • 文章目录
      • 一元左折,一元右折
      • 二元左折,二元右折
      • 可变参表达式
      • 简化打印参数
      • 一元左折,一元右折
      • 二元左折,二元右折
      • 可变参表达式
      • 简化打印参数

函数模板(第一部分)

定义函数模板

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

使用函数模板

std::cout << max(7,42) << std::endl;std::cout << max(1.1,2.2) << std::endl;std::cout << max("math","mathematics") << std::endl;
12345

模板不是被编译成可以处理任何类型的单个函数。相反,编译器会针对每一个使用该模板的类型生成对应的函数。例如,max(7,42)的调用在语义上相当于调用了:

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

double、string同理。

样例
// 函数模板的推断#include <iostream>
using namespace std;
template <typename T>
void Function(T arg)
{cout << "template<T>   " << arg << endl;
}
void Function(int arg)
{cout << "oridary <int> " << arg << endl;
}
int main(int argc, char **argv)
{// oridary <int> 13// template<T>   123// template<T>   13// template<T>   13Function(13);// 当普通函数和函数模板都符合时,普通函数的优先级更高Function("123");Function<int>(13); // 可以强制使用模板Function<>(13);    // 可以自动推导
}
123456789101112131415161718192021222324252627

将模板参数替换成具体参数类型的过程叫做instantiation,这个过程会产生一个instance of template

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

两阶段翻译 Two-Phase Translation

如果某一特定参数类型不支持模板内的操作,那么编译阶段会报错,例如:

std::complex<float> c1,c2;        //不支持 max中的 < 操作,编译阶段会报错
...
max(c1,c2);
123

模板会分成两个阶段进行”编译“: 1. 在不进行模板instantiationdefinition time阶段,此时会忽略模板参数,检查如下方面: * 语法错误,包括缺失分号。 * 使用未定义参数。 * 如果static assertion不依赖模板参数,会检查是否通过static assertion. 2. 在instantiation阶段,会再次检查模板里所有代码的正确性,尤其是那些依赖模板参数的部分。

例如:

template<typename T>
void foo(T t) {undeclared();         // first-phase compile-time error if undeclared() unknownundeclared(t);       // second-phase compile-time error if undeclared(T) unknownstatic_assert(sizeof(int) > 10,"int too small");      // first-phase compile-time errorstatic_assert(sizeof(T) > 10, "T too small");        // second-phase compile-time error}
1234567891011

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

模板的编译和链接问题

大多数人会按照如下方式组织非模板代码: 将类或者其他类型声明放在头文件(.hpp、.H、.h、.hh、.hxx)中。 将函数定义等放到一个单独的编译单元文件中(.cpp、.C、.c、.cc、.cxx)。

但是这种组织方式在包含模板的代码中却行不通,例如: 头文件:

// myfirst.hpp
#ifndef MYFIRST_HPP
#define MYFIRST_HPP
// declaration of template
template<typename T>
void printTypeof (T const&);
#endif // MYFIRST_HPP
1234567

定义函数模板的文件:

// myfirst.cpp
#include <iostream>
#include <typeinfo>
#include "myfirst.hpp"
// implementation/definition of template
template<typename T>
void printTypeof (T const& x) {std::cout << typeid(x).name() << '\n';
}
123456789

在另一个文件中使用该模板:

// myfirstmain.cpp
#include "myfirst.hpp"
// use of the template
int main() {double ice = 3.0;printTypeof(ice); // call function template for type double
}
1234567

在c/c++中,当编译阶段发现一个符号(printTypeof)没有定义只有声明时,编译器会假设它的定义在其他文件中,所以编译器会留一个”坑“给链接器linker,让它去填充真正的符号地址。

但是上面说过,模板是比较特殊的,需要在编译阶段进行instantiation,即需要进行模板参数类型推断,实例化模板,当然也就需要知道函数的定义。但是由于上面两个cpp文件都是单独的编译单元文件,所以当编译器编译myfirstmain.cpp时,它没有找到模板的定义,自然也就没有instantiation

解决办法就是我们把模板的声明和定义都放在一个头文件。大家可以看一下自己环境下的vector等STL源文件,就是把类的声明和定义都放在了一个文件中。


多模板参数

template<typename T1, typename T2>
T1 max (T1 a, T2 b) {return b < a ? a : b;
}
...
auto m = max(4, 7.2);       // 注意:返回类型是第一个模板参数T1 的类型
123456

但是问题正如注释中说的,max的返回值类型总是T1。如果我们调用max(42, 66.66),返回值则是66。

一般有三个方法解决这个问题:

  • 引入额外模板参数作为返回值类型
  • 让编译器自己找出返回值类型
  • 将返回值声明为两个模板参数的公共类型,比如int和float,公共类型就是float
引入额外模板参数作为返回值类型

在函数模板的参数类型推导过程中,一般我们不用显式指定模板参数类型。但是当模板参数不能根据传递的参数推导出来时,我们就需要显式的指定模板参数类型。

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

RT是不能根据函数的参数列表推导出来的,所以我们需要显式的指定:

max<int, double, double>(4, 7.2);
1

或者我们改变模板参数列表顺序,这种情况只需显式的指定一个参数类型即可:

template<typename RT typename T1, typename T2>      //RT变为第一个模板参数
RT max(T1 a, T2 b);   
...
max<double>(4, 7.2);
1234
让编译器自己找出返回值类型

在C++11中,我们可以利用auto和trailing return type来让编译器找出返回值类型:

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

decltype后面的文章会讲到,这里只需知道它可以获取到表达式的类型。

我们可以写的更简单点:

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

关于?:返回值规则可以参考这个:Conditional Operator: ? :

看到true ? a : b不要奇怪为什么是true,这里的重点不是计算返回值,而是得到返回值类型。

在C++14中,我们可以省略trailing return type:

template<typename T1, typename T2>
auto max (T1 a, T2 b) {return b < a ? a : b;
}
1234
将返回值声明为两个模板参数的公共类型

c++11新特性std::common_type可以产生几个不同类型的共同类型,其实核心意思跟上面说的差不多:

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

在c++14中,可以更简单的写:

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

这里使用_t后缀让我们不用写typename::type。类似的还有_v,这个在c++14的type traits里很常见。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

样例
// /// <summary>
// ///引入额外模板参数作为返回值类型
// 让编译器自己找出返回值类型
// 将返回值声明为两个模板参数的公共类型,比如int和float,公共类型就是float
#include <iostream>
using namespace std;
namespace test01
{template <typename RT, typename T2, typename T>RT add(const T2 &x, const T &y){return x + y;}
}
namespace test02
{template <typename T2, typename T>auto add(const T2 &x, const T &y) -> decltype(x + y){return x + y;}
}
namespace test03
{template <typename T2, typename T>typename std::common_type<T2, T>::type add(const T2 &x, const T &y){return x + y;}}
namespace test04
{template <typename T2, typename T>std::common_type_t<T2, T> add(const T2 &x, const T &y){return x + y;}}
int main(int argc, char **argv)
{/// @brief 引入额外模板参数作为返回值类型/// @param argc/// @param argv/// @returncout << test01::add<float, float, float>(11.2, 22.2) << endl;cout << test01::add<int, float, float>(11.2, 22.2) << endl;/// @brief 让结果自己推断返回值,让编译器自己找出返回值类型/// @param argc/// @param argv/// @returncout << test02::add<float, int>(22.5, 35) << endl;cout << test02::add<int, int>(22, 35) << endl;/// @brief 将返回值声明为两个模板参数的公共类型/// @param argc/// @param argv/// @returncout << test03::add<float, int>(22.5, 35) << endl;cout << test03::add<int, int>(22, 35) << endl;/// @brief 这里使用_t后缀让我们不用写typename和::type/// @param argc/// @param argv/// @returncout << test04::add<float, int>(22.5, 35) << endl;cout << test04::add<int, int>(22, 35) << endl;
}
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768

默认模板参数

这个很像函数的默认参数,直接看例子:

template <typename T1, typename T2, typename RT = std::common_type_t<T1, T2>>
RT max(T1 a, T2 b) {return b < a ? a : b;
}auto a = max(4, 7.2);
auto b = max<double,int,long double>(7.2, 4);
1234567

正如第二个用法,如果我们想显示的指明RT的类型,必须显示的指出全部三个参数类型。但是与函数默认参数不同的是,我们可以将默认参数放到第一个位置:

template <typename RT = long, typename T1, typename T2> 
RT max(T1 a, T2 b) {return b < a ? a : b;
}int i;
long l;max(i, l);                     // 返回值类型是long (RT 的默认值)
max<int>(4, 42);      //返回int,因为其被显式指定
12345678910
样例
#include <iostream>
using namespace std;
namespace test01
{template <typename T1, typename T2, typename RT = std::common_type_t<T1, T2>>RT max(T1 a, T2 b){return b < a ? a : b;}}
namespace test02
{template <typename RT = long, typename T1, typename T2> //RT max(T1 a, T2 b){return b < a ? a : b;}}
int main(int argc, char **argv)
{/// @brief  函数模板的默认参数/// @param argc/// @param argv/// @returnauto a = test01::max(4, 7.2);auto b = test01::max<double, int, long double>(7.2, 4);auto c = test02::max(4, 7.2);auto d = test02::max<double, int, long double>(7.2, 4);cout << a << " " << b << endl;cout << c << " " << d << endl;
}
123456789101112131415161718192021222324252627282930313233

重载函数模板

这个跟普通函数重载也类似:

#include <iostream>
using namespace std;
template <typename T>
T Max(T a, T b)
{std::cout << "Max<T,T>()\n";return b < a ? a : b;
}
#if 1template <typename T>
T Max(T a, T b, T c)
{cout << "Max<T,T,T>()" << endl;return Max(Max(a, b), c);
}#endif // !1
template <typename T>T Max(T *a, T *b)
{cout << "Max<T*>()\n";return (*b) < (*a) ? (*a) : (*b);
}
int main()
{/// @brief 函数模板的重载/// @returncout << Max<>(15, 5) << endl;int a = 55, b = 99;cout << Max<>(&a, &b) << endl;cout << Max<>(25, 23, 78) << endl;
}
1234567891011121314151617181920212223242526272829303132333435

ps. 由于函数模板重载,所以函数模板并不像类模板一样可以进行偏特化。

还有两点关于重载的基本原则需要了解一下:

重载时最好不要随便改变模板参数个数`,`最好可以显示的指定模板参数类型

模板函数特化

有时通用的函数模板不能解决个别类型的问题,我们必须对此进行定制,这就是函数模板的特化。函数模板的特化必须把所有的模版参数全部指定。

#include <iostream>
using namespace std;
/// @brief 函数模板的全特化
/// @param argc
/// @param argv
/// @return
namespace internal
{template <typename T, typename U>auto func(T arg, U arg2){return arg + arg2;}template <>auto func(string x, string y){return x + y;}/// 函数模板的全特化是根据函数模板来的,所以参数不能多或少(但类型可以变化),// 但是函数体是可以改变的// template <>// auto func(string x, string y, string z)// {//     return x + y + z;// }// template<>// auto func(string x)// {//     return x ;// }auto func(string x, int y){return new string(x, y);}auto func(int x, int y){return new string(x, y);}}
int main(int argc, char **argv)
{cout << internal::func(20, 30) << endl;cout << internal::func("test", 30) << endl;cout << internal::func(string("test"), string("Code"));
}12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849

非类型模板参数

因为T前边有一个typename/calss ,这表示T代表一个类型,是一个类型参数。

那么在模板参数列表里边,还可以定义非类型参数;非类型参数代表的是一个值。

既然非类型参数代表一个值,那么我们肯定不能用typename/class这种关键字来修饰这个值。

我们当然要用以往学习过的传统类型名来指定费类型参数。比如你非类型参数S,如果是个整型,那么就用 int s。

当模板被实例化时,这种非类型模板参数的值,或者是用户提供的,或者是编译器推断的,都有可能。

但是这些值都必须是常量表达式。因为实例化这些模板实在编译器编译时进行的。

template <int a, int b>
int func2()
{int he = a + b;return he;
}int result = func2<2, 3>(); //显示指定模板参数——用<>提供额外信息int i = 12;
int result2 = func2<i, 3>(); //不可以,报错,必须给定编译时就能确定的值这里i是在运行 时才能确定的值
123456789101112

实例化模板实在编译时进行的不是运行时

template <typename T, int a, int b>
int func3(T c)
{int result = (int)c + a + b;return result;
}int i = func3<int, 11, 12>(13);int i = func3<double, 11, 12>(13); //这里系统会以<>传递的类型为准,将13转成double型
1234567891011

这里 L1与L2的值是编译器推断出来的

template <unsigned L1, unsigned L2>
int charscomp(const char (&p1)[L1], const char (&p2)[L2])
{return strcmp(p1, p2);
}int result3 = charscomp("test1","test"); //没有提供费类型模板参数,系统会根据test1 //的长度6个,test的长度5个,来取代L1,L2
12345678

模板函数可以是内联函数

template <typename T, int a, int b>
inline
int func3(T c)
{int result = (int)c + a + b;return result;
}
1234567

模板定义并不会导致编译器生成代码,只有在我们调用这个函数模板时,使用编译器为我们实例化了一个特定版本的函数之后编译器才会生成代码。
编译器生成代码的时候,需要能够找到函数的函数体,所以,函数模板的定义通常都是在 .h 中


这里 L1与L2的值是编译器推断出来的

template <unsigned L1, unsigned L2>
int charscomp(const char (&p1)[L1], const char (&p2)[L2])
{return strcmp(p1, p2);
}int result3 = charscomp("test1","test"); //没有提供费类型模板参数,系统会根据test1 //的长度6个,test的长度5个,来取代L1,L2
12345678

模板函数可以是内联函数

template <typename T, int a, int b>
inline
int func3(T c)
{int result = (int)c + a + b;return result;
}
1234567

模板定义并不会导致编译器生成代码,只有在我们调用这个函数模板时,使用编译器为我们实例化了一个特定版本的函数之后编译器才会生成代码。
编译器生成代码的时候,需要能够找到函数的函数体,所以,函数模板的定义通常都是在 .h 中


C++泛型编程类模板,变量模板,别名模板,CSATD(第二部分)

类模板声明、实现与使用

声明:
template <typename T>
class Stack {
private:std::vector<T> elems; // elements
public:void push(T const &elem); // push elementvoid pop();               // pop elementT const &top() const;     // return top elementbool empty() const {      // return whether the stack is emptyreturn elems.empty();}
};
123456789101112
实现:
template <typename T>
void Stack<T>::push(T const &elem) {elems.push_back(elem); // append copy of passed elem
}template <typename T>
void Stack<T>::pop() {assert(!elems.empty());elems.pop_back(); // remove last element
}template <typename T>
T const &Stack<T>::top() const {assert(!elems.empty());return elems.back(); // return copy of last element
}
12345678910111213141516
使用:
int main() {Stack<int> intStack;            // stack of intsStack<std::string> stringStack; // stack of strings// manipulate int stackintStack.push(7);std::cout << intStack.top() << '\n';// manipulate string stackstringStack.push("hello");std::cout << stringStack.top() << '\n';stringStack.pop();
}
12345678910111213
注意
  • 在类声明内的构造函数、拷贝构造函数、析构函数、赋值等用到类名字的地方,可以将Stack<T>简写为Stack,例如:
template<typename T>
class Stack {...Stack (Stack const&);                           // copy constructorStack& operator= (Stack const&);      // assignment operator
...
};
1234567

但是在类外,还是需要Stack<T>:

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

注意: 拷贝构造函数(构造函数)在类内声明类外定义,类名是不需要加上模板名的。因为构造函数的定义就是函数名就是类名

例子

#include <iostream>
#include <vector>
#include <assert.h>
using namespace std;
namespace detail
{template <typename T>class Stack{private:std::vector<T> elems; // elementspublic:void push(T const &elem); // push elementvoid pop();               // pop elementT const &top() const;     // return top elementbool empty() const{ // return whether the stack is emptyreturn elems.empty();}Stack() = default; //Stack(Stack const &stack);  //拷贝构造函数Stack &operator=(Stack const &stack);};template <typename T>void Stack<T>::push(T const &elem){elems.push_back(elem); // append copy of passed elem}template <typename T>void Stack<T>::pop(){assert(!elems.empty());elems.pop_back(); // remove last element}template <typename T>T const &Stack<T>::top() const{assert(!elems.empty());return elems.back(); // return copy of last element}template <typename T>Stack<T>::Stack(Stack<T> const &stack){;}/// 注意构造函数名字的stack后面不要加<T>,构造函数的定义就是类名作为函数名// Stack<T>::Stack<T>(Stack<T> const &stack)// {//     ;// }template <typename T> //重载赋值运算符Stack<T> &Stack<T>::operator=(Stack<T> const &stack) {}/// 这里不是构造函数,所以用到stack 的地方都添加了<T>}int main(int argc, char **argv)
{detail::Stack<int> intStack;            // stack of intsdetail::Stack<std::string> stringStack; // stack of strings// manipulate int stackintStack.push(7);std::cout << intStack.top() << '\n';// manipulate string stackstringStack.push("hello");std::cout << stringStack.top() << '\n';stringStack.pop();
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475

注意: 拷贝构造函数(构造函数)在类内声明类外定义,类名是不需要加上模板名的。因为构造函数的定义就是函数名就是类名


类模板的参数推导

Deduction Guides

我们可以使用Deduction Guides来提供额外的模板参数推导规则,或者修正已有的模板参数推断规则。

Stack(char const*) -> Stack<std::string>;Stack stringStack{"bottom"};           // OK: Stack<std::string> deduced since C++17
123

例子

#include <iostream>
using namespace std;
namespace detail
{template <typename T>class A{public:A(T x, T y){cout << "A" << x << " " << y << endl;}     };
}
// 自定义推断指南
// https://blog.csdn.net/qq_38158479/article/details/122902315
namespace detail2
{template <typename T>class B{public:T mb;T mbb;};template <typename T>B(T) -> B<T>;  //自定义推断指南template <typename T>B(T, T) -> B<T>; //自定义推断指南}
namespace detail3
{template <typename T>class C{public:T mc;};}int main(int argc, char **argv)
{// C17 可以推断类模板了detail::A a(15, 16); //使用的是编译器根据构造函数 自己合成的推断指南detail2::B b{18};   //使用的是自定义的推断指南,类B没有提供构造函数,所以不会合成推断指南。需要自己提供detail2::B bb{19, 25};detail3::C c{18};   // error 没有提供构造函数,也没有提供推断指南。所以会报错
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051

类模板实参推导

  • C++17支持类模板类型推导(class template argument deduction,在下面的文章中,我叫做CTAD)。
  • 而我们在很久之前就有了template argument deduction,但是只能用于函数,这多少有点不公平。
  • 此篇博客的内容来自cppcon2018_CTAD 。
//before C++17
std::pair<int, string> p1(3, "string");
auto p2 = make_pair(3, "string");
//deduction pair<int, const char*>//C++17 or late
std::pair p3(3, string("hello")); // nice !12345678

CTAD是如何工作的?

  • CTAD工作的具体细节是很复杂的。而且有些细节是给编译器实现者使用的。
  • 这里,我将介绍CTAD工作的最重要的两步。
template <class T, class U>
struct pair{T first;U second;pair(const T& first_, const U& second_):first(first_), second(second_){}pair(T&& first_, U&& second_):first(std::forward<T>(first_)),second(std::forward<U>(second_)){}//...
};std::pair p(3, string("hello")); //how does it work ?1234567891011121314151617

上面是粗略的std::pair的实现,我们使用它来进行讲解。

第一步:

  • 当编译器看到你尝试去初始化一个p,编译器又看见了pair是一个模板的名字。但是你没有显式传入模板实参,而且pair没有默认值,所以编译器需要CTAD。
  • 编译器会去查看pair的构造函数,它会假装构造函数是普通的函数模板,像下面这样,(函数模板是可以推断参数类型的)
	template <class T, class U>  //来自pair类pair(const T& first_, const U& second_):first(first_), second(second_){}template <class T, class U> //来自pair类pair(T&& first_, U&& second_) //右值引用,不是转发引用:first(std::forward<T>(first_)),second(std::forward<U>(second_)){}1234567891011
  • 编译器会假装synthesis(合成,函数重载的术语)两个上面的函数,将类的模板参数列表加到构造函数的头部。
  • 然后编译器就使用模板实参推导,overload resolutions等一系列方法,去分辨函数重载中最合适的那一个。
  • 最终,匹配了第二个右值引用的pair,然后编译器就会推导出p的类型为pair< int, string >。

第二步:

  • 注意,第一步中,没有进行任何的实例化,仅仅是推导出模板参数。
  • 实例化发生在第二步。
  • 编译器现在有了p的类型,pair<int, string >,然后就可以调用第二个构造函数实例化出该对象。

参考:

https://zhuanlan.zhihu.com/p/338652651

http://t.csdn.cn/jQsYQ

https://zh.cppreference.com/w/cpp/language/class_template_argument_deduction

https://www.bilibili.com/video/BV1kW41117uw/?p=128&vd_source=9d5a4bc24b5e6d9bbf4a20a5dcd10b18

https://www.youtube.com/playlist?list=PLHTh1InhhwT6V9RVdFRoCG_Pm5udDxG1c

类模板的泛化; 类模板的偏特化; 类模板的全特化; 静态成员变量的全特化; 普通成员变量的全特化

#include <iostream>
using namespace std;
namespace detail
{// 类模板的全特化template <typename T, typename U>class TC{public:TC() { cout << "TC 的泛化版本" << endl; }void func() { cout << "func的泛化版本" << endl; }};template <>class TC<int, int> // 特化版本类名之后有<>{public:TC() { cout << "TC<int,int> 的特化版本" << endl; }int func(){cout << "func的特化版本" << endl;return 0;}};
}
namespace detail2
{template <typename T, typename U>class TC{public:TC() { cout << "TC 的泛化版本" << endl; }void func() { cout << "func的泛化版本" << endl; }static int m_age; // 静态成员变量的声明};template <typename T, typename U>int TC<T, U>::m_age = 18; // 静态变量的定义template <>int TC<int, double>::m_age = 80; // 静态变量的全特化template <>void TC<string, double>::func(){cout << "普通成员函数TC<double,int>::functest1的全特化" << endl;return;}// template <>// class TC<string, double>// {如果进行了普通成员函数的全特化,或者是静态成员变里的全特化// ,那么,就无法用这些全特化时指定的类型来对整个类模板进行全特化// };// template <>// class TC<int, double>// {/// 如果进行了普通成员函数的全特化,或者是静态成员变里的全特化// ,那么,就无法用这些全特化时指定的类型来对整个类模板进行全特化// };template <>class TC<int, char>{}; // 这种特化由于类型不是  普通成员函数特化的<string ,douoble> 和 静态变量全特化的<int,double> ,所以是可行的
}
namespace detail3
{template <typename T, typename U>class TC{public:TC() { cout << "TC 的泛化版本" << endl; }void func() { cout << "func的泛化版本" << endl; }};// 范围上的偏特化template <typename T, typename U>class TC<const T *, const U *>{public:TC() { cout << "TC范围上 的偏特化版本" << endl; }void func() { cout << "func的范围偏特化版本" << endl; }};// 数量上的偏特化template <typename U>class TC<int, U>{public:TC() { cout << "TC 的数量上的偏特化版本" << endl; }void func() { cout << "func的数量偏特化版本" << endl; }};}
int main(int argc, char **argv)
{// 调用泛化版本detail::TC<float, float> tc;tc.func();// 调用特化detail::TC<int, int> tc2;tc.func();// 调用普通成员函数的全特化detail2::TC<string, double> tc3;tc3.func();// 调用静态变量的全特化detail2::TC<int, double> tc4;cout << tc4.m_age;// 如果进行了普通成员函数的全特化,或者是静态成员变里的全特化// ,那么,就无法用这些全特化时指定的类型来对整个类模板进行全特化// 因为对普通成员函数的全特化,或者是静态成员变量的全特化 就是生成了对应的参数的类的全特化版本// 调用范围上的偏特化版本detail3::TC<const int *, const int *> b;b.func();// 调用数量上的偏特化版本detail3::TC<int, double> c;c.func();
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120

类模板的默认参数

类型别名

非类型模板参数的使用


#include <iostream>
using namespace std;
namespace detail
{ // 默认版本的泛化template <typename T, typename U>class TC{public:TC(){cout << "泛化版本的构造函数" << endl;}void func(){cout << "泛化版本的函数" << endl;}};
#if 0template <typename T, typename U>class TC<T *, T *= int *>{// 类模板偏特化版本中的类型模板参数不可以有缺省值。public:TC(){cout << "范围特化版本的构造函数" << endl;}void func(){cout << "特化版本的函数" << endl;}};template <typename T>class TC<int, T = double>{// 类模板偏特化版本中的类型模板参数不可以有缺省值。public:TC(){cout << "数量特化版本的构造函数" << endl;}void func(){cout << "特化版本的函数" << endl;}};#endif
}
namespace detail2
{// 常规缺省template <typename T, typename U = const int *>class TC{public:TC(){cout << "泛化版本的构造函数" << endl;}void func(){cout << "泛化版本的函数" << endl;}};}
namespace detail3
{ // 后面的模板参数依特前面的模板参数template <typename T, typename U = T *>class TC{public:TC(){cout << "泛化版本的构造函数" << endl;}void func(){cout << "泛化版本的函数" << endl;}};}
#if 0
namespace detail4
{
// 类型模板参数缺省值的规矩:如果某个模板参数有缺省值,那么从这个有缺省值的模板参数开始,后面的所有模板参数都得有缺省值。template <typename T=const double*, typename U>class TC{public:TC(){cout << "泛化版本的构造函数" << endl;}void func(){cout << "泛化版本的函数" << endl;}};}#endif
namespace detail5
{// 类模板声明中,缺省参数要先给右边参数提供缺省值,再给左边提供// template<typename T=int,typename T2>// class T ;// 在声明中指定缺省值// 声明1template <typename U, typename T, typename Z, typename Y, typename X = int>class TC;// 声明2template <typename U, typename T, typename Z, typename Y = double, typename X> // 这里的X 已经被指定默认参数了class TC;// 声明3template <typename U, typename T = U *, typename Z = const int *, typename Y, typename X = int> // 这里的X Y已经被指定默认参数了class TC;// 声明4template <typename U, typename T = double *, typename Z, typename Y, typename X = int> // 这里的X Y Z已经被指定默认参数了class TC;// 声明5template <typename U, typename T, typename Z, typename Y, typename X = int> // 这里的X Y Z T 已经被指定默认参数了class TC{};}
namespace detail6
{template <typename U, typename T = double *, typename Z = int, typename Y = int, typename X = int> // 这里的X Y Z已经被指定默认参数了class TC{};// 方式一定义别名typedef TC<int, double, int, const int *, double> i_D_I_CI_D; // 定义类型别名// 方式二定义别名using i_D_I_CI_D2 = TC<int, double, int, const int *, double>;
}
namespace detail7
{template <typename T, typename U, size_t arrsize = 8>class TC{public:T m_aee[arrsize];void func();};template <typename T, typename U, size_t arrsize>void TC<T, U, arrsize>::func(){cout << "func" << endl;}
}
namespace detail8
{template <double VAT>double process(double v){return v * VAT;}// error: a non-type template parameter cannot have type 'double'template <std::string name>class TemStr{public:std::string _str = name;};// error: a non-type template parameter cannot have type 'std::string'template <char const *name>class TemPtr{public:std::string _str = name;};// extern char const* sa = "test";//ERROR// error: non-type template argument of type 'const char *' is not a constant expression// extern char const sa[] = "test";//OK// 浮点和string直接不使用也会编译error.// 全局字符数组是一个外部链接对象,可以作为非类型模板参数。// 非类型模板参数类型是有限制的,通常是常整数,枚举值,或者指向外部链接对象的指针。 浮点数应为历史原因不能作为非类型模板参数,以后可能会支持。//     字符串文字是内部链接对象,类对象更不能作为非类型模板参数。//类类型不能作为非类型模板参数
class a{};
template<typename T, a size>
class myarray{};
}
int main(int argc, char **argv)
{// 类模板中的缺省参数detail2::TC<int> tc;tc.func();detail3::TC<double> tc2;tc2.func();// 声明中缺省detail5::TC<int, int, int, int, int> tc3;detail5::TC<int> tc4;detail5::TC<int, double> tc5;detail5::TC<int, double, float> tc6;detail5::TC<int, double, float, const int> tc7;// 使用类型别名detail6::i_D_I_CI_D tc7;detail6::i_D_I_CI_D2 tc8;// 非类型模板参数detail7::TC<int, int> tc9;for (size_t i = 0; i < 8; i++){tc9.m_aee[i] = static_cast<int>(i);}cout << tc9.m_aee[7] << endl;
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220

成员函数模板;模板类虚函数;模板虚函数;拷贝构造函数;拷贝构造函数模板;赋值运算符;赋值运算符模板

#include <iostream>
using namespace std;
namespace detail
{// 成员函数模板template <typename T>class TC{public:template <typename T2>T2 func(T2 arg1, T2 arg2) // 成员函数模板{return arg1 + arg2;}void func2() // 成员函数{cout << this->num1 << " " << this->num2 << endl;}int num1 = 300;double num2 = 1.1;};
}
namespace detail2
{
#if 0 // 模板类虚函数template <class T>class A{public:virtual ~A() {}virtual void foo(T &t) {}};template <class T, class R>class B : public A<T>{R *r;public:void foo(T &t) override {}};//     /类模板中可以有普通的虚成员函数(虚函数),这并没有什么问题。大家都知道,普通成员函数如果不被调用的情况下不会被实例化出来。
// /但是,对于虚函数,不管是否调用,编译器都会把他实例化出来,因为编译器要创建虚函数表t1,该表中的每个具体表项都对应一个
// /虚函数地址,所以编译器必然得把所有虚函数都实例化出来。
#endif#if 0class A // 模板虚函数{public:virtual ~A() {}template <class T>virtual void foo(T &t) {}};class B : public A{public:template <class T>void foo(T &t) override {}};// 解析虚函数调用的最流行方法是使用表(vtable"),其中每个虚函数都映射到表中的一个索引。这// 或多或少需要您知道表的大小。// 使用模板,将在不同的模块中根据需要创建新功能。然后,您要么必须说服链接器在计算出函数的// 最终数量后构建表,要么使用某种运行时结构在运行时搜索可用函数。// 在许多系统上,链接器是操作系统的一部分,对C++一无所知,因此该选项受到限制。运行时搜索// 当然会对性能产生负面影响,也许对所有虚函数都是如此。// 因此,最终决定不值得将虚拟模板引入语言中。// 来自C++模板完整指南:// 成员函数模板不能声明为虚拟的。施加此约束是因为虚函数调用机制的通常实现使用固定大// 小的表,每个虚函数有一个条目。但是,直到整个程序被翻译后,成员函数模板的实例化次// 数才固定下来。因此,支持虚拟成员函数模板需要在C++编译器和链接器中支持一种全新// 的机制。相比之下,类模板的普通成员可以是虚拟的,因为在实例化类时它们的数量是固定// 的//     C++父的说法:如果允许虚函数模板,则每次有人用新的参数类型调用该虚函数模板时,就必须给对应的虚函数表再增加一项,这意味// //只有链接程序才能去构造虚函数表并在表中设置有关函数,因此,成员函数模板绝不能是虚的。
#endif}
namespace detail
{// 类(包括模板类)构造函数是真实的构造函数;然而模板构造函数,其实质是模板函数。// 两者不能混为一谈。在一个模板类中,构造函数和模板构造函数同时存在时,优先调用构造函数。// 只有当确切符合模板构造函数的接口时,才调用模板构造函数。编译器永远不会把模板构造函数视为构造函数,// 即使客户没有自己定义拷贝构造函数,编译器也会生成一个默认的拷贝构造函数(而不会调用拷贝构造函数模板),这种情况同样存在于拷贝赋值函数和模板拷贝赋值函数。// 请看下面的例子:#include <iostream>using namespace std;template <typename T>class TempClass{public:T d;TempClass<T>(T _d = 0) : d(_d) { cout << "This is TempClass Constructor1. " << endl; };template <typename O> // 模板拷贝构造函数TempClass<T>(const TempClass<O> &_tmp) : d(_tmp.d){cout << "This is a template constructor, not a TempClass Constructor." << endl;};// template <typename O> // 模板拷贝构造函数//     TempClass<T>(TempClass<O> &_tmp) : d(_tmp.d)//     {//         cout << "This is a template constructor, not a TempClass Constructor." << endl;//     };};}
namespace detail2
{// 类(包括模板类)构造函数是真实的构造函数;然而模板构造函数,其实质是模板函数。// 两者不能混为一谈。在一个模板类中,构造函数和模板构造函数同时存在时,优先调用构造函数。// 只有当确切符合模板构造函数的接口时,才调用模板构造函数。编译器永远不会把模板构造函数视为构造函数,// 即使客户没有自己定义拷贝构造函数,编译器也会生成一个默认的拷贝构造函数(而不是去调用拷贝构造函数模板),这种情况同样存在于拷贝赋值函数和模板拷贝赋值函数。// 请看下面的例子:#include <iostream>using namespace std;template <typename T>class TempClass{public:T d;// 两个构造函数,其中第二个是拷贝构造函数TempClass<T>(T _d = 0) : d(_d) { cout << "This is TempClass Constructor1. " << endl; };TempClass<T>(TempClass<T> &_tmp) : d(_tmp.d) { cout << "This is TempClass Constructor2. " << endl; };template <typename O> // 模板构造函数TempClass<T>(TempClass<O> &_tmp) : d(_tmp.d){cout << "This is a template constructor, not a TempClass Constructor." << endl;};};}
namespace detail3
{
// 在拷贝构造函数和拷贝构造函数中不使用const 的情况
#include <iostream>using namespace std;template <typename T>class TempClass{public:T d;// 两个构造函数,其中第二个是拷贝构造函数TempClass<T>(T _d = 0) : d(_d) { cout << "This is TempClass Constructor1. " << endl; };// TempClass<T>(TempClass<T> &_tmp) : d(_tmp.d) { cout << "This is TempClass Constructor2. " << endl; };template <typename O> // 模板构造函数TempClass<T>(TempClass<O> &_tmp) : d(_tmp.d){cout << "This is a template constructor, not a TempClass Constructor." << endl;};};
}
namespace detail4
{template <typename T1>// 拷贝赋值运算符class A{public:template <typename U>A<T1> &operator=(const A<U> &other){cout << "operator=(const A<T1> &other) 拷贝赋值运算符执行了! " << endl;return *this;}public:T1 t1;A(T1 t1) : t1(t1) { cout << "t1" << endl; };A() = default;};
}
int main(int argc, char **argv)
{detail::TC<int> tc;//   调用成员函数模板cout << tc.func(100, 200) << endl;// 调用成员函数tc.func2();// 1类模板中的成员函数,只有源程序代码中出现调用这些成员函数的代码时,这些成员函数才会出现在一个实例化了的类模板中// 2类模板中的成员函数模板,只有源程序代码中出现调用这些成员函数模板的代码时,这些成员函数模板的具体实例才会出现在一个实例化了的类中detail::TempClass<int> a;detail::TempClass<int> b(a); // detail中 没有拷贝构造函数,但也不会去调用拷贝构造函数模板,而是生成默认的拷贝构造函数来调用detail2::TempClass<double> c(); // 调用模板拷贝构造函数cout << "================================================================" << endl;detail2::TempClass<int> aa;detail2::TempClass<int> bb(aa); // 当类型相同时候调用拷贝构造函数(调用detail2 中已有的),即使在该模板类中用户没有自定义该函数,编译器也会生成一个默认拷贝构造函数。因为编译器永远不会认为一个模板构造函数是一个构造函数detail2::TempClass<double> cc(aa); // 当类型不同时,调用模板拷贝构造函数/**
300
300 1.1
This is TempClass Constructor1.
This is a template constructor, not a TempClass Constructor.
================================================================
This is TempClass Constructor1.
This is TempClass Constructor2.
This is a template constructor, not a TempClass Constructor.*/cout << "===============detail3===============" << endl;detail3::TempClass<int> aaa;detail3::TempClass<int> bbb(aaa);detail3::TempClass<double> ccc(aaa);// 当不使用const 的时候,类型相同和类型不同都会调用拷贝构造函数模板,没有拷贝构造函数,就会去调用拷贝构造函数模板/*===============detail3===============
This is TempClass Constructor1.
This is a template constructor, not a TempClass Constructor.
This is a template constructor, not a TempClass Constructor.*/cout << "===========detail4===============" << endl;detail4::A<int> a4(15);detail4::A<int> b4;b4 = a4; // 和拷贝构造函数相同,类型相同调用默认生成的赋值运算符,而不是赋值运算符模板cout << "b::" << b4.t1 << endl;detail4::A<double> c4; // 类型不同时,调用赋值运算符模板c4 = a4;
}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241

成员函数的泛化版本;成员函数的特化版本;类外定义的全特化版本; 函数模板的嵌套;类模板的嵌套

#include <iostream>
using namespace std;
#if 0
namespace detail
{template <typename T>class TC{public:template <typename T1, typename T2>void func(T1 a, T2 b){cout << "func()泛化版本" << endl;}template <typename T2>void func(int a, T2 b){cout << "func()偏特化版本" << endl;}template <>  void func(int a, double b){cout << "func()全特化版本" << endl;}TC(int args, int args2) : args(args), args2(args2){};int args, args2 = 0;};
}
#endif
#if 0
namespace detail2
{   //类模板之外,定义成员函数的全特化版本template <typename T>class TC{public:template <typename T1, typename T2>void func(T1 a, T2 b);template <typename T2>void func(int a, T2 b);template <>void func(int a, double b);TC(int args, int args2) : args(args), args2(args2){};int args, args2 = 0;};template <typename T>template <typename T1, typename T2>void TC<T>::func(T1 a, T2 b){cout << "func()泛化版本" << endl;}template <typename T>template <typename T2>void TC<T>::func(int a, T2 b){cout << "func()偏特化版本" << endl;}template <typename T>template <>     // //将会报错,有些资料上说目前的C+标准不允许在类模板之外全特化一个未被特化的类模板(指的是类模板A)的成员函数void func(int a, double b){cout << "func()全特化版本" << endl;}
}
#endif// detail2 的解决方法, 将类TC 进行全特化, 使用这个全特化的TC的成员函数, 特化这个成员函数是被允许的
namespace detail3
{template <typename T>class TC{public:template <typename T1, typename T2>void func(T1 a, T2 b);template <typename T2>void func(int a, T2 b);template <>void func(int a, double b);TC(int args, int args2) : args(args), args2(args2){};int args, args2 = 0;};template <typename T>template <typename T1, typename T2>void TC<T>::func(T1 a, T2 b){cout << "func()泛化版本" << endl;}template <typename T>template <typename T2>void TC<T>::func(int a, T2 b){cout << "func()偏特化版本" << endl;}// template <typename T>// template <>// void func(int a, double b)//{//     cout << "func()全特化版本" << endl;// }// 类模板的全特化版本template <>class TC<int>{template <typename T1, typename T2>void func(T1 a, T2 b);template <typename T2>void func(int a, T2 b);template <>void func(int a, double b);TC(int args, int args2) : args(args), args2(args2){};int args, args2 = 0;// 并不需要在特化的TC类中 写全特化成员函数func的声明/*template<>void func(int a,double b);*/};template <>void TC<int>::func(int a, double b){cout << "func()全特化版本" << endl;}
}
namespace detail4
{// 函数模板嵌套template <class _Ty1, class _Ty2>class MM{public:MM(_Ty1 one, _Ty2 two) : one(one), two(two) {}friend ostream &operator<<(ostream &out, const MM &mm){out << mm.one << " " << mm.two;return out;}protected:_Ty1 one;_Ty2 two;};template <class _Ty> // 这个函数模板以另一个模板类型的数据为参数void print(_Ty data){cout << data << endl; // 需要运算符重载}int main(){// 隐式调用print(MM<string, int>("小芳", 32)); // 在用类模板时显式写出来// 显示调用// 函数模板可以隐式调用 但是需要知道自己传的是什么类型 模板类型:MM<string, int>print<MM<string, int>>(MM<string, int>("小美", 28));// 起别名简化代码using MMType = MM<string, int>;// 显示调用优化print<MMType>(MMType("小美", 28));return 0;}/*输出*/小芳 32 小美 28
}
namespace detail
{// 类模板嵌套template <class _Ty1, class _Ty2>class MM{public:MM(_Ty1 one, _Ty2 two) : one(one), two(two) {}friend ostream &operator<<(ostream &out, const MM &mm){out << mm.one << " " << mm.two;return out;}protected:_Ty1 one;_Ty2 two;};template <class _Ty1, class _Ty2> // 再写一个类去操作数据class Data{public:Data(_Ty1 one, _Ty2 two) : one(one), two(two) {}void print(){cout << one << " " << two << endl;}protected:_Ty1 one;_Ty2 two;};void testFunc(){// Data类的实例化   _Ty1 one: MM<string,int>   _Ty2 two: MM<double,double>Data<MM<string, int>, MM<double, double>>data(MM<string, int>("小芳", 18), MM<double, double>(89, 56));data.print(); // 传入两个对象 分别用 _Ty1 和 _Ty2 构造// 上面两行 等效下面四行代码 先把对象构建出来再传入对象即可MM<string, int> mmData("小芳", 18);MM<double, double> mmScore(89, 56);Data<MM<string, int>, MM<double, double>> mData(mmData, mmScore);mData.print();}int main(){testFunc();}/*输出*/小芳 18 89 56 小芳 18 89 56
}
int main(int argc, char **argv)
{// detail::TC<int> tc(1, 2);// tc.func(1, 2.0);// tc.func(2, "string");// tc.func("str", "ing");
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231

类模板显式实例化定义/声明

类模版的隐式实例化

  • 模板(Templet)并不是真正的函数或类,它仅仅是编译器用来生成函数或类的一张“图纸”。模板不会占用内存,最终生成的函数或者类才会占用内存。由模板生成函数或类的过程叫做模板的实例化(Instantiate),相应地,针对某个类型生成的特定版本的函数或类叫做模板的一个实例(Instantiation)。
  • 模板的实例化是按需进行的,用到哪个类型就生成针对哪个类型的函数或类,不会提前生成过多的代码。也就是说,编译器会根据传递给类型参数的实参(也可以是编译器自己推演出来的实参)来生成一个特定版本的函数或类,并且相同的类型只生成一次。实例化的过程也很简单,就是将所有的类型参数用实参代替。
  • 另外需要注意的是类模板的实例化,通过类模板创建对象时并不会实例化所有的成员函数,只有等到真正调用它们时才会被实例化;如果一个成员函数永远不会被调用,那它就永远不会被实例化。这说明类的实例化是延迟的、局部的,编译器并不着急生成所有的代码。
  • 通过类模板创建对象时,一般只需要实例化成员变量和构造函数。成员变量被实例化后就能够知道对象的大小了(占用的字节数),构造函数被实例化后就能够知道如何初始化了;对象的创建过程就是分配一块大小已知的内存,并对这块内存进行初始化。

例子
在代码中实际使用模板类构造对象或者调用模板函数时,编译器会根据调用者传给模板的实参进行模板类型推导然后对模板进行实例化,此过程中的实例化即是隐式实例化。

template<typename T>
T add(T t1, T2)
{return t1 + t2;
}template<typename T>
class Dylan
{
public:T m_data;
};int main()
{int ret = add(3,4);//隐式实例化,int add<int>(int t1, int t2);Dylan<double> dylan;//隐式实例化
}
123456789101112131415161718

2. 显式实例化声明、定义

extern template int add<int>(int t1, int t2);//显式实例化声明
extern template class Dylan<int>;            //显式实例化声明template int add<int>(int t1, int t2);       //显式实例化定义
template class Dylan<int>;                   //显式实例化定义
12345
  • 当编译器遇到显式实例化声明时,表示实例化定义在程序的其他地方(相对于当前cpp文件)即在其他某一个cpp文件中定义,因此不再按照模板进行类型推导去生成隐式实例化定义。
  • 当编译器遇到显式实例化定义时,根据定义所提供的模板实参去实例化模板,生成针对该模板实参的实例化定义。
3. 显式实例化的用途

模板类、函数通常定义在头文件中,这些头文件会被很多cpp文件包含,在这些cpp文件中会多次使用这些模板,比如下面的例子:

//template.hpp
template<typename T>
class Dylan
{
public:T m_data;
};//test1.cpp
#include "template.hpp"
Dylan<int> t1;
Dylan<int> t2;//test2.cpp
#include "template.hpp"
Dylan<int> t3;
Dylan<int> t4;
1234567891011121314151617

在test1.cpp/test2.cpp 中多次实例化了Dylan类,按说编译完后的可执行程序中会包含多份Dylan的定义,然而实际上,整个程序中却只有一份Dylan的定义。这个处理是在编译和链接过程中实现的,目前主流的实现模式有两种:

a. Borland模式

Borland模式通过在编译器中加入与公共块等效的代码来解决模板实例化问题。在编译时,每个文件独立编译,遇到模板或者模板的实例化都不加选择地直接编译。在链接的时候将所有目标文件中的模板定义和实例化都收集起来,根据需要只保留一个。这种方法实现简单,但因为模板代码被重复编译,增加了编译时间。在这种模式下,我们编写代码应该尽量让模板的所有定义都放入头文件中,以确保模板能够被顺利地实例化。要支持此模式,编译器厂商必须更换支持此模式的链接器。

b. Cfront模式

AT&T编译器支持此模式,每个文件编译时,如果遇到模板定义和实例化都不直接编译,而是将其存储在模板存储库中(template repository)。模板存储库是一个自动维护的存储模板实例的地方。在链接时,链接器再根据实际需要编译出模板的实例化代码。这种方法效率高,但实现复杂。在这种模式下,我们应该尽量将非内联成员模板的定义分离到一个单独的文件中,进行单独编译。

在一个链接器支持Borland模式的编译目标(编译后的可执行文件)上,g++使用Borland模式解决实例化问题。比如ELF(Linux/GNU), Mac OS X, Microsoft windows, 否则,g++不支持上述两种模式。

如何避免Borland模式的缺点?

上面我们说g++实现的是Borland 模式,由于我们为每一份实例化生成代码,这样在大型程序中就有可能包含很多重复的实例化定义代码,虽然链接阶段,链接器会剔除这些重复的定义,但仍然会导致编译过程中的目标文件(或者共享库文件)过于庞大。这时候,我们就可以通过C++11的模板显式实例化的方法解决。看下面的代码:

// template.hpp
template<typename T>
class Dylan
{
public:Dylan(T t);T m_data;
};// template.cpp
#include "template.hpp"
template<typename T>
Dylan<T>::Dylan(T t)
{m_data = t;
}template class Dylan<int>; //模板实例化定义// main.cpp
#include "template.hpp"
extern template class Dylan<int>; //模板实例化声明,告诉编译器,此实例化在其他文件中定义//不需要根据模板定义生成实例化代码  int main()
{Dylan<int> dylan(3);//OK, 使用在template.cpp中的定义Dylan<float> dylan1(3.0);//error, 引发未定义错误
}
12345678910111213141516171819202122232425262728293031
  • 上面的代码中,我们将模板类的具体定义放在template.cpp中,并且在template.cpp中通过显式实例化定义语句具体化了Dylan。在main.cpp中,我们通过显式实例化声明告诉编译器,Dylan将在其他文件中定义,不需要在本文件中根据template.hpp的类模板实例化Dylan。
  • 由于我们没有针对Dylan做显式实例化的声明和定义,因此Dylan
    dylan(3.0)会根据template.hpp中的类模板定义进行隐式实例化,然而构造函数是在template.cpp文件中定义的,在template.hpp中找不到构造函数的定义,因而报错。如果把构造函数的定义挪回template.hpp,那Dylan就能通过编译了。
  • Note:在编译中,如果指定-fno-implicit-templates,编译器就会禁止隐式实例化,从而只使用显式实例化。
注意
  • 模板显式实例化会将这种类型的类模板及所有成员函数都实例化出来,包括内联成员函数。
例子

hpp 文件(被多次调用的类模版)

#ifndef __test00HPP__
#define __test00HPP__#include <iostream>
#include <vector>
using namespace std;
template <typename T1>
class Test00
{
public:template <typename T2=T1>Test00(T2 num1, T2 num2);public:template <typename T3=T1>void mytest(T3 tmpt);T1 testNum;
};template <typename T1>
template <typename T2>
Test00<T1>::Test00(T2 num1, T2 num2)
{cout << "num1=" << num1 << ",num2=" << num2 << endl;
}template <typename T1>
template <typename T3>
void Test00<T1>::mytest(T3 tmpt)
{cout << "tmpt=" << tmpt << endl;
}
#endif1234567891011121314151617181920212223242526272829303132333435

main.cpp (使用该hpp模板 文件的一个cpp文件,同时也是这个程序的主文件)

#include <iostream>
#include "test00.hpp"
#include "test01.h"template Test00<float>;
//模板实例化定义
int main()
{Test00<float> fTest00(1, 2);fTest00.mytest(55);fTest00.testNum = 100.123;cout << fTest00.testNum << endl;myTest01();return 0;
}
1234567891011121314151617

test01.cpp(使用该hpp模板 文件的一个cpp文件)

#include "test01.h"using namespace std;
extern template Test00<float>;
//模板实例化声明,告诉编译器,此实例化在其他文件中定义//不需要根据模板定义生成实例化代码  
void myTest01()
{Test00<float> fTest00(1, 2);
}
12345678910

test01.h (分文件编写的方式,)

#ifndef __TEST01_H__
#define __TEST01_H__#include "test00.hpp"void myTest01();#endif
12345678

注意,当我们进行显式实例化(template Test00;)后
hpp 文件中将会实例化出

    void mytest(float tmpt);
void Test00<float>::mytest(float tmpt)
{cout << "tmpt=" << tmpt << endl;
}Test00<float>::Test00(float num1, float  num2)
{cout << "num1=" << num1 << ",num2=" << num2 << endl;
}
1234567891011

模板的编译和链接问题

大多数人会按照如下方式组织非模板代码: 将类或者其他类型声明放在头文件(.hpp、.H、.h、.hh、.hxx)中。 将函数定义等放到一个单独的编译单元文件中(.cpp、.C、.c、.cc、.cxx)。

但是这种组织方式在包含模板的代码中却行不通,例如: 头文件:

// myfirst.hpp
#ifndef MYFIRST_HPP
#define MYFIRST_HPP
// declaration of template
template<typename T>
void printTypeof (T const&);
#endif // MYFIRST_HPP
定义函数模板的文件:// myfirst.cpp
#include <iostream>
#include <typeinfo>
#include "myfirst.hpp"
// implementation/definition of template
template<typename T>
void printTypeof (T const& x) {std::cout << typeid(x).name() << '\n';
}
在另一个文件中使用该模板:// myfirstmain.cpp
#include "myfirst.hpp"
// use of the template
int main() {double ice = 3.0;printTypeof(ice); // call function template for type double
}
123456789101112131415161718192021222324252627

在c/c++中,当编译阶段发现一个符号(printTypeof)没有定义只有声明时,编译器会假设它的定义在其他文件中,所以编译器会留一个”坑“给链接器linker,让它去填充真正的符号地址。

但是上面说过,模板是比较特殊的,需要在编译阶段进行instantiation,即需要进行模板参数类型推断,实例化模板,当然也就需要知道函数的定义。但是由于上面两个cpp文件都是单独的编译单元文件,所以当编译器编译myfirstmain.cpp时,它没有找到模板的定义,自然也就没有instantiation。

解决办法就是我们把模板的声明和定义都放在一个头文件。

变量模板

变量模板的定义,泛化,全特化,偏特化

// 一:定义,泛化,全特化,偏特化,使用方法
#include <iostream>using namespace std;// 变量模板的泛化版本template <typename T> //(1)T g_tmp{};            // 变量的零初始化方式// 变量模板的全特化,,这里的char可以和int不一样template <> //(2)char g_tmp<float>{'a'};// 变量模板的偏特化,从参数范围上的偏特化,必须要求是T*,依赖于Ttemplate <typename T> //(3)T g_tmp<T *>{10};// 变量模板的偏特化,从参数范围上的偏特化,必须要求是T*,依赖于Ttemplate <typename T> //(4)T g_tmp<const T *>{100};int main(){/*变量模板:Variable Templates,c++14引入,一般写在.h文件当中从感觉上,变量模板与函数模板有些类似,看起来象一个没有参数,但是有返回值的函数模板*/// 变量模板的,泛化,偏特化,以及全特化和使用g_tmp<int> = 20;std::cout << g_tmp<int> << std::endl;g_tmp<float>;std::cout << g_tmp<float> << std::endl;g_tmp<int *>;std::cout << g_tmp<int *> << std::endl;g_tmp<const int *>;std::cout << g_tmp<const int *> << std::endl;return 0;}}int main(){TMP<int>::m_i<int> = 100;return 0;}
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950

变量模板在C++14 之前的实现

方式一
// 变量模板在C++14 之前的实现
namespace detail4
{// https : // blog.csdn.net/lanchunhui/article/details/49835213//  C++14 前变量模板的实现//  第一种替代方案是,使用类模板的constexpr static数据成员的方式:template <typename T>struct PI{constexpr static T pi = T(3.1415926535897932385);// 这里必须使用关键字constexpr,而不可以是const// const 常量必须在编译器得到确定// 自C++11起,constexpr可以让表达式核定于编译期};// duplicate declarationtemplate <typename T>constexpr T PI<T>::pi;int main(int, char **){std::cout << PI<int>::pi << std::endl;// 3std::cout << PI<double>::pi << std::endl;// 3.14159return 0;}// 这种做法,因为constant是一种ODR(One Definition Rule)的定义规则。对constant的两次声明是有必要的,一次在类模板体内,一次在类模板体外,如上代码所示。
}
123456789101112131415161718192021222324252627282930
方式二
///变量模板在C++14 之前的实现
namespace detail5
// https: // blog.csdn.net/lanchunhui/article/details/49835213
// 另外一种解决方案是使用constexpr函数模板的形式,该函数返回期待的类型。
{template <typename T>T PI(){constexpr T pi = T(3.1415926535897932385);return pi;}int main(int, char **){std::cout << PI<int>() << std::endl;std::cout << PI<double>() << std::endl;return 0;}
}
12345678910111213141516171819

C++14 的变量模板

变量模板的作用
namespace detail6
{// 如果对模板或者C++标准感兴趣的开发者们相信都不会对变量模板感到陌生,我们今天就讲一讲变量模板// 从C++14 开始,变量也可以被某种类型参数化。称为变量模板。// 例如可以通过下面的代码定义pi,但是参数化了其类型:template <typename T = int> // 我们写作默认intT pi{};                     // 初始化列表 为0int main(){std::cout << precision(16);pi<int> = 20;                      // pi<>=20;效果一样std::cout << pi<int> << std::endl; // 20pi<double> = 3.14159265358;std::cout << pi<double> << std::endl; // 3.14159265358std::cout << pi<float> << std::endl;  // 0return 0;}// 注意,和其它几种模板类似,这个定义不要出现在函数内部或者块作用域内部。// 那么我们这样使用它,它是什么?我们没有创建局部变量,注意,它不是局部变量,这是一个全局变量,我们这个过程是实例化了一个全局变量。// 而且我们必须显性声明模板参数。// 我们讲讲它的一些用处// 打个比方,如果要使用一个类的静态数据,必须像下面这样调用#include <iostream>
#include <string>template <typename T>class MyClass{public:static constexpr int max = 1000;};int main(){MyClass<int>::max;}// 得加上作用域解析运算符,但是,有了变量模板之后,我们可以#include <iostream>
#include <string>template <typename T>class MyClass{public:static constexpr int max = 1000;};// 这意味着对于一个标准库的类:template <typename T>int myMax = MyClass<T>::max;namespace std{template <typename T>class numeric_limits{public:static constexpr bool is_signed = false;};}// 可以定义:template <typename T>constexpr bool isSigned = std::numeric_limits<T>::is_signed;int main(){// 应用工程师就可以使用下面这样的代码:auto i = myMax<std::string>;std::cout << i << std::endl;// 而不是:// auto i = MyClass<std::string>::max;// std::cout << i << std::endl;system("pause");return 0;}}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081

C++17 类型特性后缀

namespace detail7
{// https : // blog.csdn.net/a4364634611/article/details/124663903//     类型特性后缀_v// C++17以后,标准库使用变量模板的技术为标准库中所有的类型特性定义便捷的用法,比如:// std::is_const_v<T>   // since C++17// 代替// std::is_const<T>::value  // since C++11// 标准库定义如下:// namespace std// {//     template<typename T> constexpr bool is_const_v = is_const<T>::value;// }// 熟悉标准库的开发者,对于 #include<type_traits> 不会陌生,我们举两个简单的例子template <typename T>autofunc(T &a){std::cout << typeid(a).name() << " ";if (std::is_array_v<T>){std::cout << "是数组" << std::endl;}elsestd::cout << "不是数组" << std::endl;}template <typename T>auto func2(T &v){std::cout << typeid(v).name() << " ";if (std::is_const_v<T>){std::cout << "是const" << std::endl;}elsestd::cout << "不是const" << std::endl;}// 这第一种相当于使用了变量模板,也有不使用的写法template <typename T>auto func(T &a){std::cout << typeid(a).name() << " ";if (std::is_array<T>::value){std::cout << "是数组" << std::endl;}elsestd::cout << "不是数组" << std::endl;}template <typename T>auto func2(T &v){std::cout << typeid(v).name() << " ";if (std::is_const<T>::value){std::cout << "是const" << std::endl;}elsestd::cout << "不是const" << std::endl;}// 发现了吗?第二种相当于我们一开始介绍的使用作用域解析运算符来取出的值,标准委员会保留老式的写法,但是也更新形式的方法,大家理解即可。// 仅供参考,如有错误还请指正}
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576

变量模板的默认模板参数; 变量模板的非类型模板参数

namespace detail
{template <typename T>constexpr T pi{3.1415926535897932385};
}
namespace detail2
{// 变量模板可以有默认模板实参:template <typename T = long double>constexpr T pi = T{3.1415926535897932385};
}
namespace detail3
{// 变量模板可以由非类型参数进行参数化,这可以用于参数化初始值:template <int N>std::array<int, N> arr{}; // array with N elements, zero initializedtemplate <auto N>constexpr decltype(N) dval = N; // type of dval depends on passed value
}123456789101112131415161718192021
int main(int argc, char **argv)
{// 使用变量模板,必须指定它的类型:std::cout << detail::pi<double> << '\n';std::cout << detail::pi<float> << '\n';// 使用默认值或者其他类型std::cout << detail2::pi<> << '\n';      // outputs a long doublestd::cout << detail2::pi<float> << '\n'; // outputs a float// 但必须使用尖括号,仅仅使用pi是错误的:// std::cout << detail2::pi << '\n'; // ERROR// 变量模板可以由非类型参数进行参数化,这可以用于参数化初始值std::cout << detail3::dval<'c'> << '\n'; // N has value 'c' of type chardetail3::arr<10>[0] = 42;                                 // sets first element of global arrfor (std::size_t i = 0; i < detail3::arr<10>.size(); ++i) // uses values set in arr{std::cout << detail3::arr<10>[i] << '\n';}
}
12345678910111213141516171819202122

别名模板

namespace detail8
{// 别名模板// https://blog.csdn.net/zwvista/article/details/54612025// 别名模板(alias template)// 别名模板:带模板参数的类型别名// 类型别名(type alias)// 是C++ 11新引入的语法形式: using newtype = oldtype;// 在语法功能上,它相当于传统C / C++ 语言中的typedef语句: typedef oldtype newtype;// 可以看出,类型别名中原有类型和别名的定义顺序与typedef语句正好相反。除此之外,类型别名与typedef语句还有一点不同,类型别名可以加上模板参数形成别名模板 : template <typename...>//                                                                                                                                                        using newtype = oldtype<...>;// 注:C++ 11引入类型别名意图取代typedef语句的原因在于:无法直接在typedef语句的基础上直接构建别名模板。这是因为typedef语句自身存在某些局限性,直接给typedef加上模板参数会带来语法解析上的问题。  template <typename T, typename U>struct A;template <typename T>struct B{typedef A<T, int> type;};template <typename T>using C = A<T, int>;template <typename T>using D = typename B<T>::type;// 代码说明:// 假设我们有一个带两个模板参数T和U的类模板A。现在我们需要声明一个只带一个模板参数T的类模板,使其等价于模板参数U为int类型的A模板。也就是说,我们需要一个模板参数T任意,模板参数U为int类型的A模板的别名,或者说A<T, int>的别名。// 在C++11之前,答案为类模板B。要定义类型别名,必然要使用typedef。但由于typedef不能带模板参数,所以typedef必须被嵌入到一个带模板参数的类模板里面。在模板参数为T的类模板B里面,类型type被定义成A<T, int>的别名。也就是说typename B<T>::type被定义成了A<T, int>的别名。// 在C++11之后,答案为别名模板C。类型别名直接就可以带模板参数。C<T>直接被定义成了A<T, int>的别名。// 如果出于某种原因,在定义别名的时候无法使用类模板A而只能使用类模板B,别名模板也能发挥作用。这里D<T>直接被定义成了typename B<T>::type的别名。由于后者是A<T, int>的别名,所以D<T>其实也是A<T, int>的别名。// 这段代码展示了别名模板的主要用途:1.为部分模板参数固定的类模板提供别名。2.为类模板中嵌入的类型定义提供别名。
}
namespace detail9
{// 一:定义,泛化,全特化,偏特化,使用方法
#include <iostream>using namespace std;// 变量模板的泛化版本template <typename T> //(1)T g_tmp{};            // 变量的零初始化方式// 变量模板的全特化,,这里的char可以和int不一样template <> //(2)char g_tmp<float>{'a'};// 变量模板的偏特化,从参数范围上的偏特化,必须要求是T*,依赖于Ttemplate <typename T> //(3)T g_tmp<T *>{10};// 变量模板的偏特化,从参数范围上的偏特化,必须要求是T*,依赖于Ttemplate <typename T> //(4)T g_tmp<const T *>{100};int main(){/*变量模板:Variable Templates,c++14引入,一般写在.h文件当中从感觉上,变量模板与函数模板有些类似,看起来象一个没有参数,但是有返回值的函数模板*/// 变量模板的,泛化,偏特化,以及全特化和使用g_tmp<int> = 20;std::cout << g_tmp<int> << std::endl;g_tmp<float>;std::cout << g_tmp<float> << std::endl;g_tmp<int *>;std::cout << g_tmp<int *> << std::endl;g_tmp<const int *>;std::cout << g_tmp<const int *> << std::endl;return 0;}}
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
//typedef给固定类型起别名
typedef std::map<std::string, int> map_s_i;
map_s_i mymap;
mymap.insert({"first", 1});typedef std::map<std::string, std::string> map_s_s;
map_s_s mymap2;
mymap2.insert({"first", "second"});//c++98通过类模板实现类型名不固定
template<typename T>
struct map_s {typedef std::map<std::string, T> type;
};
map_s<int>::type map;
map.insert({"first", 1});//c++11
template<typename T>
using map_s = std::map<std::string, T>;
map_s<int> map;
map.insert({"first", 1});//using用来给类型有关模板起别名
//using包含了typedef的所有功能
typedef unsigned int uint_t;
using uint_t = unsigned int;typedef std::map<std::string, int> map_s_i;
using map_s_i = std::map<std::string, int>;typedef int(*FunType)(int,int);
using FunType = int(*)(int,int);int RealFunc(int i, int j) {return 3;}
template<typename T>
using myfunc_M = int(*)(T,T);
myfunc_M<int> pointFunc;//类型名,非类模板实例化后的类
pointFunc = RealFunc;
cout <<pointFunc(1, 6) <<endl;123456789101112131415161718192021222324252627282930313233343536373839404142

成员变量模板

namespace detail10
// 成员变量模板
{
#include <iostream>// #include <boost/type_index.hpp>using namespace std;template <typename T>class TMP{public:template <typename U>// U m_i = {}; 只可以使用静态成员变量static U m_i;};template <typename T>template <typename U>U TMP<T>::m_i = 10;int main(){TMP<int>::m_i<int> = 100;return 0;}
}
1234567891011121314151617181920212223242526

类模板的友元(第三部分)

  • 让某个类B称为另外一个类A的友元类,这样的话,类B就可以在其成员函数中访问类A的所有成员不管这些成员在类A中是用什么(public,protected,private)来修饰的。
  • 如果现在类A和类B都变成了类模板,那么能否让类模板B成为类模板A的友元类模板呢?

  • 让类模板的某个实例(具体的类)成为友元类
template <typename U> class B;  -- 类模板B的声明template <typename T>
class A
{	friend class B<long>;     -- 不需要任何public,private等修饰符。
private:int data;
};template <typename U>
class B
{
public:void callBAF(){A<int> atmpobj;atmpobj.data = 5;cout << atmpobj.data << endl;}
};
  • 调用:
B<long> bobj;bobj.callBAF();  // 5
  • 让类模板B特定的实例成为了类模板A的友元类。

  • *让类模板成为友元类模板*
template <typename T>
class A
{	template<typename> friend class B;
private:int data;
};template <typename U>
class B
{
public:void callBAF(){A<int> atmpobj;atmpobj.data = 5;cout << atmpobj.data << endl;}
};
  • 调用:
B<long> bobj1;B<int> bobj2;bobj1.callBAF();  //5bobj2.callBAF();  //5

  • 让类型模板参数成为友元类
    • C++11新标准中引入:如果传递进来的 类型模板参数 是一个类类型,则这个类类型可以成为当前类模板的友元类。
template <typename T>
class A2
{friend T;
private:int data;
};class CF
{
public:void callCFAF(){A2<CF> aobj1;    -- 让CF类成为了A2aobj1.data = 12;cout << aobj1.data << endl;}
};
  • 调用:
CF mycfobj;mycfobj.callCFAF();
  • 输出:
12
  • 代码行A2 aobj1; 的效果是让CF类成为了A2类的友元类;

  • 于是,

    在CF类的成员函数中(不是在其他域,如主函数中)

    ,可以

    直接访问

    aobj1这个A2类对象的data

    私有成员变量

    • 如在main函数中直接访问A私有成员,编译报错
A2<_nmsp2::CF> aobj1;aobj1.data = 12;

  • 如果传递给类模板A2的类型模板参数不是一个类类型,那么代码行friend T;就会被忽略。
template <typename T>
class A2
{friend T;
private:int data;
};class CF
{
public:void callCFAF(){
-- 因为CF类并不是A2<int>的友元类,自然不能在这里访问aobj2这个A2<int>类对象的data私有成员变量。A2<int> aobj2; aobj2.data = 15;cout << aobj2.data << endl;}
};
  • 调用报错:
CF mycfobj;mycfobj.callCFAF();

  • 增加 friend class CF;则将整个CF类作为友元类
template <typename T>
class A2
{friend T;friend  class CF;private:int data;
};class CF
{
public:void callCFAF(){A2<CF> aobj1;     //让CF类成为了A2<CF>aobj1.data = 12;cout << aobj1.data << endl;A2<int> aobj2; aobj2.data = 15;cout << aobj2.data << endl;}
};
  • 调用:
CF mycfobj;mycfobj.callCFAF();
  • 输出:
1215

让某个类B称为另外一个类A的友元类,这样的话,类B就可以在其成员函数中访问类A的所有成员不管这些成员在类A中是用什么(public,protected,private)来修饰的。
如果现在类A和类B都变成了类模板,那么能否让类模板B成为类模板A的友元类模板呢?

让类模板的某个实例(具体的类)成为友元类

template <typename U> class B;  -- 类模板B的声明template <typename T>
class A
{	friend class B<long>;     -- 不需要任何public,private等修饰符。
private:int data;
};template <typename U>
class B
{
public:void callBAF(){A<int> atmpobj;atmpobj.data = 5;cout << atmpobj.data << endl;}
};
123456789101112131415161718192021
  • 调用:
B<long> bobj;
bobj.callBAF();  // 5
12
  • 让类模板B特定的实例成为了类模板A的友元类。

让类模板成为友元类模板

template <typename T>
class A
{	template<typename> friend class B;
private:int data;
};template <typename U>
class B
{
public:void callBAF(){A<int> atmpobj;atmpobj.data = 5;cout << atmpobj.data << endl;}
};
12345678910111213141516171819
  • 调用:
B<long> bobj1;
B<int> bobj2;
bobj1.callBAF();  //5
bobj2.callBAF();  //5
1234

让类型模板参数成为友元类

C++11新标准中引入:如果传递进来的 类型模板参数 是一个类类型,则这个类类型可以成为当前类模板的友元类。

template <typename T>
class A2
{friend T;
private:int data;
};class CF
{
public:void callCFAF(){A2<CF> aobj1;    -- 让CF类成为了A2aobj1.data = 12;cout << aobj1.data << endl;}
};
12345678910111213141516171819
  • 调用:
CF mycfobj;
mycfobj.callCFAF();
12
  • 输出:
 12
1
  • 代码行A2 aobj1; 的效果是让CF类成为了A2类的友元类;
  • 于是,在CF类的成员函数中(不是在其他域,如主函数中),可以直接访问aobj1这个A2类对象的data私有成员变量
    • 如在main函数中直接访问A私有成员,编译报错
A2<_nmsp2::CF> aobj1;
aobj1.data = 12;
12
  • 如果传递给类模板A2的类型模板参数不是一个类类型,那么代码行friend T;就会被忽略。
template <typename T>
class A2
{friend T;
private:int data;
};class CF
{
public:void callCFAF(){
-- 因为CF类并不是A2<int>的友元类,自然不能在这里访问aobj2这个A2<int>类对象的data私有成员变量。A2<int> aobj2; aobj2.data = 15;cout << aobj2.data << endl;}
};
12345678910111213141516171819
  • 调用报错:
CF mycfobj;
mycfobj.callCFAF();
12
增加 friend class CF;则将整个CF类作为友元类
template <typename T>
class A2
{friend T;friend  class CF;private:int data;
};class CF
{
public:void callCFAF(){A2<CF> aobj1;     //让CF类成为了A2<CF>aobj1.data = 12;cout << aobj1.data << endl;A2<int> aobj2; aobj2.data = 15;cout << aobj2.data << endl;}
};
123456789101112131415161718192021222324
  • 调用:
CF mycfobj;
mycfobj.callCFAF();
12
  • 输出:
12
15

让函数模板的某个实例成为友元函数

-- 函数模板func的声明
template <typename U, typename V> 	void func(U val1, V val2);//Men类模板
template <typename Z>
class Men
{friend void func<int, int>(int, int);  -- <int,int>是两个模板实参-- friend void func<>(int, int);    这种写法也可以-- friend void func<int>(int, int);  这种写法也可以friend void func<float,int>(float, int);--friend void func<>(float, int);friend void func<int, float>(int, float);-- friend void func<>(int, float);private:void funcmen() const{cout << "Men::funcmen被调用了" << endl;}
};template <typename U,typename V>
void func(U val1, V val2)
{Men<int> mymen;mymen.funcmen(); 
}
12345678910111213141516171819202122232425262728293031
  • 调用
func(2, 3);
func<float>(4.6f, 5); 
func<int, float>(4, 5.8f)
123
  • 输出:
Men::funcmen被调用了
Men::funcmen被调用了
Men::funcmen被调用了
1234

让函数模板func成为类模板Men的友元函数模板

-- Men类模板
template <typename Z>
class Men
{-- 让函数模板func成为类模板Men的友元函数模板template <typename U, typename V> friend void func(U val1, V val2);private:void funcmen() const{cout << "Men::funcmen被调用了" << endl;}
};template <typename U,typename V>
void func(U val1, V val2)
{Men<int> mymen;mymen.funcmen(); 
}
1234567891011121314151617181920
  • 将func函数模板(泛化版本)声明为Men类模板的友元模板之后,那么func函数模板的特化版本也会被看成是Men类模板的友元
//func全特化版本
template <>
void func(int val1, double val2)
{Men<int> mymen;mymen.funcmen();
}123456789
  • 编译器会把全特化的func函数模板看待成一个实例化过的函数模板。

在类模板中定义友元函数

  • 这种友元函数是能够被调用的,而且也只有在代码中调用了函数的时候,编译器才会实例化出这个函数。
  • 之所以这样定义友元函数,一般都是因为在该友元函数中会 用到这个类模板的成员。
  • 这种友元函数的调用与调用普通函数函数,就把他当成普通函数来看待即可。
//Men类模板
template <typename Z>
class Men
{friend void func2(Men<Z>& tmpmen){tmpmen.funcmen();}
private:void funcmen() const{cout << "Men::funcmen被调用了" << endl;}
};
1234567891011121314
  • 调用:
Men<double> mymen2;
func2(mymen2);     -- 直接调用Men类模板中定义的友元函数func2Men<int> mymen3;
func2(mymen3);
12345
  • func2在Men类模板被实例化时并不会被一并实例化出来,只有调用了func2的时候,才会被实例化出来
  • 因为func2在类模板Men,所以调用func2时,如果func2中的代码特别简单,则func2会被当成内联函数来处理
  • 如果func2中的代码比较复杂,比如出现了for循环,那么func2很可能就不会被当做内联函数来处理。
//Men类模板
template <typename Z>
class Men
{friend void func2(Men<Z>& tmpmen){for(int i= 0; i<1 ; ++i)	tmpmen.funcmen();}
private:void funcmen() const{cout << "Men::funcmen被调用了" << endl;}
};
123456789101112131415
  • func2(mymen2); 可以被实例化出 void func2(class Men&);
  • func2(mymen3); 可以被实例化出 void func2(class Men&);
    out << “Men::funcmen被调用了” << endl;
    }
    };
- func2(mymen2); 可以被实例化出  void func2(class Men<double>&);
- func2(mymen3); 可以被实例化出  void func2(class Men<int>&);
- func2**其实是个全局函数。**
1234

可变参模板指南(第四部分)

可变参函数模板

基本外观和介绍
 #include<iostream>
using namespace std;
namespace detail1{
template <typename... N>    //<1>  void func(N... args)       //<2>
{cout << "func begin" << endl;cout << "函数形参包的参数数目"<<sizeof...(args) << endl;cout <<"类型形参包的参数数目"<< sizeof...(N) << endl;cout <<"func endl" << endl;
}
}int main()
{detail1::func();detail1::func(12, 34);detail1::func(12, 3.4, 34);detail1::func(12, 34, "34");
}
123456789101112131415161718192021
  • <1>这个省略号...其实我们可以理解为 *指针&引用 一样,都是用来描述一种参数类型,我们把带省略号的参数称为参数包,它里面包含了0到N(N>=0)个模版参数,例如上面的(typename... N) 形参N是一个类型参数包,而args函数形参参数包,并且这个类型参数包函数形参参数包无法直接使用.

  • <2> 我们可以使用

    N...
    

    来使用参数包, 表示将参数包逐个展开 .

    • 把省略号...加到参数包参数的右边可以理解为将当前参数包展开成了一个一个独立的参数

与其他参数名一样,可以将这些参数包的名称指定为任何符合C++标识符规则的名称,Args与T的区别在于,T与一种类型匹配,而Args与任意数量(包括0)的类型匹配。
更准确的说,函数参数包args包含的值列表与模板参数包Args包含的类型列表匹配——无论是从数量还是类型上!

  • 这里用到了sizeof...运算符,当我们需要知道包中有多少个元素的时候,可以使用sizeof...运算符返回一个常量。

输出结果:

func begin
函数形参包的参数数目0
类型形参包的参数数目0
func endl
func begin
函数形参包的参数数目2
类型形参包的参数数目2
func endl
func begin
函数形参包的参数数目3
类型形参包的参数数目3
func endl
func begin
函数形参包的参数数目3
类型形参包的参数数目3
func endl
12345678910111213141516
展开参数包(获取参数包的值)
  • 我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。
template <class ...Args>
void ShowList(Args... args)
{// 获取、解析可变参数
}
int main()
{ShowList(1);ShowList(1, 'A');ShowList(1, 'A', std::string("sort"));return 0;
}
123456789101112
错误的演示
  • 1 并不支持这样使用:
template <class ...Args>
void ShowList(Args... args)
{cout << sizeof...(Args) << endl;cout << sizeof...(args) << endl;for (int i = 0; i < sizeof...(args); i++){// 无法编译,编译器无法解析cout << args[i] << " ";    }
}
1234567891011
  • 2 可以将省略号放在函数参数包名字的右边,将参数包展开:
template<class T>
void showlist(Args...args)
{showlist(args...);
}
12345

但是这样调用存在缺陷,假如有这样的调用:

showlist(6,'L',0.6);
1

这将把6、‘L’、0.6 封装到args中,在该函数内部,下面的调用:

showlist(args...);
1

将展开成这样:

showlist(5,'L',0.5);
1

该函数调用与原始函数调用相同,因此他将使用相同的参数不断的调用自己,导致无限递归。

正确的演示
采取递归方式
 // 同名递归终止函数,位置放在 参数包展开函数的前面void func() // 当 OtherArgs为NULL空参数 时, 调用这个函数,实际上这个函数只是为了让下面的变参函数能调用到最后的无参数的func(){cout << "递归终止函数被调用" << endl;};template <typename Z, typename... N>  // N是 模板参数包,(一包类型)void func(Z firstArg, N... OtherArgs) // firstArg 是解开的第一个参数,OthreArgs 是函数参数包 ,(一包参数){cout << "收到的参数值:" << firstArg << endl;func(OtherArgs...); // 递归调用  }
int main()
{func("test", 34, 3.4,"func");
}
12345678910111213141516
上例中的递归调用过程是这样的:
func("test", 34, 3.4,"func");
func( 34, 3.4,"func");
func( 3.4,"func");
func("func");
12345
收到的参数值:test
收到的参数值:34
收到的参数值:3.4
收到的参数值:func
递归终止函数被调用
12345
  • 每一次递归都提取出参数包里的第一个参数,直到这个参数包里只剩一个参数时,因为事先重载了同名函数,所以最后一次调用的是普通函数,即递归终止函数。

    • 因为重载的是无参的同名函数,所以程序中允许这样的调用
    int main()
    {func()
    }
    1234
    
    • 如果重载的是单参数的同名函数, 程序如下

      template <typename Z>void func(Z args) // 当 OtherArgs为一个参数 时, 调用这个函数{cout << "剩余一个参数时候被打印" << args << endl;};template <typename Z, typename... N>  // N是 模板参数包,(一包类型)void func(Z firstArg, N... OtherArgs) // firstArg 是解开的第一个参数,OthreArgs 是函数参数包 ,(一包参数){cout << "收到的参数值:" << firstArg << endl;func(OtherArgs...); // 递归调用}int main(){func("1", 2, 3, 4,  "5", 6, "7");}
      12345678910111213141516
      

      输出结果:

      收到的参数值:1
      收到的参数值:2
      收到的参数值:3
      收到的参数值:4
      收到的参数值:5
      收到的参数值:6
      剩余一个参数时候被打印7
      1234567
      
      • 但这样就不能调用无参数的函数了
       int main()
      {
      func();//error : [Error] no matching function for call to 'func0'
      }
      1234
      
    • 如果重载的是单参数和两个参数的同名函数, 程序如下

     template <typename Z>void func(Z args) // 当 OtherArgs为一个参数 时, 调用这个函数{cout << "剩余一个参数时候被打印" << args << endl;};template <typename Z, typename N>void func(Z args, N args2) // 这个函数存在的时候,不会调用上面一个参数的(因为使用上面的,还会有存在一个参数包)。当两个参数模板都适用某种情况时,优先使用没有“template parameter pack”的版本。{cout << "剩余两个参数的时候被打印"<< "  args  " << args << "  args2  " << args2 << endl;};template <typename Z, typename... N>  // N是 模板参数包,(一包类型)void func(Z firstArg, N... OtherArgs) // firstArg 是解开的第一个参数,OthreArgs 是函数参数包 ,(一包参数){cout << "收到的参数值:" << firstArg << endl;func(OtherArgs...); // 递归调用}int main(){func("1", 2, 3, 4,  "5", 6, "7");}12345678910111213141516171819202122
    
    收到的参数值:1
    收到的参数值:2
    收到的参数值:3
    收到的参数值:4
    收到的参数值:5
    剩余两个参数的时候被打印  args  6  args2  7
    123456
    

    可见没有调用 void func(Z args) 而是调用 void func(Z args, N args2)

    • 因为重载了单参函数,所以可以调用单参数的函数了

      • int main()
        {
        func("ers");
        }
        1234
        

采取逗号表达式
namespace detail4
{template <class T>  //递归终止函数int print(T t){cout << t << endl;return 0;}template <class... Args>void expand(Args... args){int arr[] = {(print(args), 0)...}; // 核心就是它}
int main()
{expand(1,2,3,4,5,6);cout<<"main end!!"<<endl;
}12345678910111213141516171819202122

逗号表达式会按顺序执行逗号前面的表达式,比如:

d = (a = b, c); 
1

这个表达式会按顺序执行:b会先赋值给a,接着括号中的逗号表达式返回c的值,因此d将等于c。

expand函数中的逗号表达式:(printarg(args), 0) , 也是按照这个执行顺序,先执行printarg(args) ,再得到逗号表达式(printarg(args), 0)的结果0。

{(print(args), 0)...} 第一步先展开print的形参参数包:

(print(args1), 0)` `(print(args2), 0)` `(print(args3), 0)` … `(print(argsN), 0)

第二步 执行逗号表达式:

(printarg(args), 0)的结果0。 所以初始化列表为 {0,0,0,0...}

  • 很明显可以看到, 我们 是在 第一步把 参数展开的,那为什么需要逗号表达式呢???

为什么需要逗号表达式

现在假设我们去掉逗号表达式,只使用第一步 展开print的形参参数包

auto arr = {(print(args)...)}` 将被展开为 `{print(args1),print(args2)print(args3)}

这里初始化列表的作用 是将展开的多个参数 聚合起来.可以使用初始化列表接受任意长度的参数

 auto arr = {(print(args)...)};报错::'std::initializer_list<auto> arr' has incomplete type  
1234
  • 不完整类型是这样一种类型,它缺乏足够的信息例如长度去描述一个完整的对象
  • 不完整类型必须通过某种方式补充完整,才能使用它们进行实例化,否则只能用于定义指针或引用

而逗号表达式 把这个表达式 print(args1) print(args2) print(args3) 的值 统一成整数0了, 也就是 (print(args), 0) 的返回值是0,这样初始化列表就像这样 auto arr = {0,0,0,0}.

既然是这样 我们只要让 print(args1) print(args2) print(args3) … 返回一个完整类型的值即可, 而且是要在编译期间就能返回确定的值

  • 我们可以借助constexpr () 函数,在编译期就返回值
#include<iostream>
using namespace std;
namespace detail4
{template <class T>  //递归终止函数
constexpr	int print(T t){cout << t << endl;return 0;}template <class... Args>void expand(Args... args){
//		auto arr = {(print(args), 0)...}; // 核心就是它auto arr ={print(args)...}; //这个参数在编译期就能确定}
}int main(){detail4::expand(1,2,3,4,5,6);cout<<"main end!!"<<endl;}
123456789101112131415161718192021222324

运行结果:

1
2
3
4
5
6
main end!!
1234567
  • 虽说constexpr函数所定义的是编译期的函数,但实际上在运行期constexpr函数也能被调用。事实上,如果使用编译期常量参数调用constexpr函数,我们就能够在编译期得到运算结果;而如果使用运行期变量参数调用constexpr函数,那么在运行期我们同样也能得到运算结果。
其他方式
template<typename T>
void printargs(T t)   //注意这不是递归终止函数!而是一个用来输出参数内容的函数
{cout << t << endl;
};template<class... Args>
void expand(Args... args)
{//方法1:数组的初始化列表//int arr[] = {(printargs(args),0)...};//逗号表达式。包扩展为(printargs(args1),0)//(printargs(args1),0),...,(printargs(argsN),0)//计算每个逗号表达式,调用printargs()(在这里获得各个参数)//同时,每个逗号表达式结果得0,然后用N个0初始化arr。//方法2:利用std::initializer_list//std::initializer_list<int>{(printargs(args),0)...}; //比方法1简洁,且无需定义一个辅助的arr。//方法3:利用lambda表达式//[&args...]{std::initializer_list<int>{(cout << args << endl,0)...};}();[&]{std::initializer_list<int>{(cout << args << endl,0)...};}();
} 12345678910111213141516171819202122

应用

1.求最大值(可接受多个参数)
//1.求最大值(可接受多个参数)int maximum(int n)   //递归终止函数
{return n;
}template<typename... Args>
int maximum(int n, Args... args) //递归函数
{return std::max(n, maximum(args...)); 
}
123456789101112
2. 用可变参数模板函数模仿printf的功能
//2. 用可变参数模板函数模仿printf的功能
void Printf(const char* s)  //递归终止函数
{while(*s){if(*s == '%' && *(++s)!='%')throw std::runtime_error("invalid format string: missing arguments");cout << *s++;    }
}template<typename T, typename... Args>
void Printf(const char* s, T value, Args...args) //递归函数,展开参数包(value + args...)
{while(*s){if(*s == '%' && *(++s)!='%'){ //找到格式控制符%cout << value;Printf(++s, args...); //call even when *s=0 to detect extra argumentreturn;}cout << *s++; //"%d %s %p %f\n"输出非格式控制字符,如空格。            }throw runtime_error("extra arguments provided to print");
}
1234567891011121314151617181920212223242526
3. 使用tuple转化并分解参数包
//3. 使用tuple转化并分解参数包
template<std::size_t Index=0, typename Tuple>  //递归终止函数
typename std::enable_if<Index==std::tuple_size<Tuple>::value>::type  //返回值void类型 
print_tp(Tuple&& t) //tp: tuple
{    
}template<std::size_t Index=0, typename Tuple>  //递归终止函数
typename std::enable_if<Index<std::tuple_size<Tuple>::value>::type  //返回值void类型 
print_tp(Tuple&& t) //tp: tuple
{cout << std::get<Index>(t) << endl;print_tp<Index+1>(std::forward<Tuple>(t));
}template<typename... Args>
void print(Args... args)
{print_tp(std::make_tuple(args...)); //将args转为tuple
}
1234567891011121314151617181920

获取可变参函数模板中的值

参考折叠表达式



可变参类模板

  • 允许模板定义中包含0到多个(任意个)模板参数

基本外观和介绍

这个使用 递归+继承的方式 实现的版本 介绍.

三段式 :
#include <iostream>
using namespace std;
// 主模板   <3>
template <typename... Args>
class Test
{
};/*    主模板也可以仅仅保留声明   // <3.1>template <typename... Args>class Test;
*/// 特化模板中展开 <4>
template <typename _Ty, typename... Args>
class Test<_Ty, Args...> : private Test<Args...> //<1> 继承父类
{
public:Test(){};Test(_Ty data, Args... args) : _data(data), Test<Args...>(args...){printf("偏特化版本执行了,this = %p,sizeof...(Others)=%d\n", this, sizeof...(args));}; //<2> 调用父类来处理args 参数
private:_Ty _data;
};int main(int argc, const char **argv)
{Test<int, float, string> one(2, 2.5f, "helloWorld");  // <6>Test<> two(); //  <7>
}
123456789101112131415161718192021222324252627282930313233343536
  • <1>
    • Test<_Ty, Args...> 继承自 Test<Args...> 代入参数 就是Test <int,double,string> 继承自 Test<double,string>
  • <2>
    • Test(_Ty data, Args... args) : _data(data), Test<Args...>(args...){}; 调用父类构造函数,
  • <3>
    • 主模板template <typename... Args>就是 泛化的类模板, 主模板是不需要实例化形参的; 有人也会把 主模板仅仅保留声明<3.1>. 主模板的存在,是为了让特化模板存在.

这里的主模板的参数接收范围是 0到多个 因为 ...Args 的范围就是0到多个,

  • <4>
    • 特化的模板是用来 参数展开的 . 特化模板的参数接收范围是 1到多个 ,<typename _Ty, typename... Args> 至少有传入一个参数才会调用它.
  • <7>
  • 执行 <7>Test<> two(); 将是不会调用任何模板的, 解决办法是还要添加一个无参的特化模板. 这个无参的特化模板也可以当成是终止递归的函数(这个后面会讲).
template <>   //调用0个参数
class Test<>
{
public:Test(){cout<<"调用无参数的"<<endl;}
};
12345678

上面的三段式模板参数是可以传入0个参数的. 因为可变参数模板中的模板参数可以有0个,但有时候0个模板参数没有意义,此时就可以选择下面的两段式.

两段式:
// 在主模板中展开 参数
template <typename _Ty, typename... OtherArgs>   // 主模板
class Test : private Test<OtherArgs...> //<1> 继承父类
{
public:Test(){};Test(_Ty FirstData, OtherArgs... args) : _data(FirstData), Test<OtherArgs...>(args...){printf("偏特化版本执行了,this = %p,sizeof...(Others)=%d\n", this, sizeof...(args));}; //<2> 调用父类来处理args 参数
private:_Ty _data;
};template<typename _Ty>
class Test<_Ty>
{
public:Test(_Ty data):_data(data){};private:_Ty _data;
};int main()
{Test<string> three("string");//Test<>test(); // error ,因为主模板接收 1到多个参数,所以不会接收 0个参数的类模板
//而且正因为主模板是接收 1到多个参数, 所以这里如果 写了 接收 0个参数的特化类模板将会报错
//[Error] wrong number of template arguments (O, should be at least 1)
//[错误]错误的模板参数的数量(O,应该是至少1)cout<<"main endl"<<endl;
}1234567891011121314151617181920212223242526272829303132333435363738
  • 和前面的三段式相比;他们的主模板是不同的;

    • 三段式是template <typename... Args>class Test 而 两段式是

      template <typename _Ty, typename... OtherArgs> 他们的可接受参数是不同的, 三段式接收 0到 多个参数

    而两段式 接收 1到多个 不能是接收 0个参数

    • 所以这里如果 写了 接收 0个参数的特化类模板将会报错[Error] wrong number of template arguments (0, should be at least 1)
      错误的模板参数的数量(0,应该是至少1)

    •  template <>   class Test<>{public:Test(){cout<<"调用无参数的"<<endl;}};
      12345678
      
  • 其次 类模板参数展开的位置不同 ;

    • 三段式是 在 特化的模板类template <typename _Ty, typename... Args> class Test<_Ty, Args...>中展开的

    而两段式是在 主模板template <typename _Ty, typename... OtherArgs> class Test 中展开的

  • 递归结束函数的写法不同

    • 三段式的递归结束函数 可以是无参的 这样 模板参数是可以传入0个参数的.
    • 三段式的递归结束函数, 不包含单参数的特化 函数,也能传入单个参数
    #include <iostream>
    using namespace std;
    // 主模板   <3>
    template <typename... OtherArgs>   //...参数包里面的个数就是 0到多个,所以这个是会接收0 个参数的
    class Test
    {};// 特化模板中展开 <4>
    template <typename _Ty, typename... OtherArgs>
    class Test<_Ty, OtherArgs...> : public Test<OtherArgs...> //<1> 继承父类
    {
    public:Test(){};Test(_Ty FirstData, OtherArgs... args) : _data(FirstData), Test<OtherArgs...>(args...){printf("偏特化版本执行了,this = %p,sizeof...(Others)=%d\n", this, sizeof...(args));}; //<2> 调用父类来处理args 参数private:_Ty _data;	};// 特化一个接收无参的类
    template <>   
    class Test<>
    {
    public:Test(){cout<<"调用无参数的"<<endl;}
    };int main()
    {
    Test<> test1; //三段式的递归结束函数 可以是无参的  这样 模板参数是可以传入0个参数的.Test<int> test2(100); //三段式的递归结束函数, 不包含单参数的特化 函数,也能传入单个参数
    }12345678910111213141516171819202122232425262728293031323334353637383940414243
    
    • 三段式的递归结束函数 也可以是单参数, 多参数的.
    #include <iostream>
    using namespace std;
    // 主模板   <3>
    template <typename... OtherArgs>   //...参数包里面的个数就是 0到多个,所以这个是会接收0 个参数的
    class Test
    {};// 特化模板中展开 <4>
    template <typename _Ty, typename... OtherArgs>
    class Test<_Ty, OtherArgs...> : public Test<OtherArgs...> //<1> 继承父类
    {
    public:Test(){};Test(_Ty FirstData, OtherArgs... args) : _data(FirstData), Test<OtherArgs...>(args...){printf("偏特化版本执行了,this = %p,sizeof...(Others)=%d\n", this, sizeof...(args));}; //<2> 调用父类来处理args 参数private:_Ty _data;};// 特化一个接收无参的类
    template <>   
    class Test<>
    {
    public:Test(){cout<<"调用无参数的"<<endl;}
    };// 特化一个接受单参的类
    template <typename _Ty>   
    class Test<_Ty>
    {
    public:Test(_Ty data):m_data(data){cout<<"调用含有一个参数的"<<endl;	};private:_Ty m_data;
    };
    // 特化一个接受双参的类
    template <typename _Ty,typename _Se>   
    class Test<_Ty,_Se>
    {
    public:Test(_Ty data,_Se data2):m_data(data),m_data2(data2){cout<<"调用含有双参数的"<<endl;	};private:_Ty m_data;_Se m_data2;
    };int main()
    {
    // 调用无参数的
    Test<> test1; //调用单参数的
    Test<int> test2 (10); //调用双参数的
    Test<int, string> test3 (10,"helloWorld"); //调用多参数Test <int,string,double,float> (100,"string",100.00,1.2f);}
    123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
    
    • 而两段式必须包含单参数的递归结束函数,否则将不能 模板参数传入单个参数.
    • 两段式 模板参数不可以传入0个参数的.
    #include <iostream>
    using namespace std;// 主模板展开 <4>
    template <typename _Ty, typename... OtherArgs>   // 主模板
    class Test : private Test<OtherArgs...> //<1> 继承父类
    {
    public:Test(){};Test(_Ty FirstData, OtherArgs... args) : _data(FirstData), Test<OtherArgs...>(args...){printf("偏特化版本执行了,this = %p,sizeof...(Others)=%d\n", this, sizeof...(args));}; //<2> 调用父类来处理args 参数
    private:_Ty _data;
    };template<typename _Ty,typename _Last>
    class Test<_Ty, _Last>
    {
    public:Test(_Ty data, _Last data2):m_data(data),m_data2(data2){cout<<"调用了两个参数的"<<endl;};private:_Ty m_data;_Last m_data2;};
    /*    单参数的特化模板被注释掉, 就不能再传入单个参数的模板参数
    template<typename _Ty>
    class Test<_Ty>
    {
    public:Test(_Ty data):m_data(data){cout<<"调用单参数的"<<endl;};
    private:_Ty m_data;};
    */
    int main()
    {Test<> test; //  error两段式  模板参数不可以传入0个参数的.   Test<int> test2(100); // error  而两段式必须包含单参数的递归结束函数,否则将不能 模板参数传入单个参数.
    }123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
    
    • 两段式 不能特化 无参的模板类 (因为主模板最少只能接收一个参数)
    #include <iostream>
    using namespace std;// 主模板展开 <4>
    template <typename _Ty, typename... OtherArgs>   // 主模板
    class Test : private Test<OtherArgs...> //<1> 继承父类
    {
    public:Test(){};Test(_Ty FirstData, OtherArgs... args) : _data(FirstData), Test<OtherArgs...>(args...){printf("偏特化版本执行了,this = %p,sizeof...(Others)=%d\n", this, sizeof...(args));}; //<2> 调用父类来处理args 参数
    private:_Ty _data;
    };template<typename _Ty,typename _Last>
    class Test<_Ty, _Last>
    {
    public:Test(_Ty data, _Last data2):m_data(data),m_data2(data2){cout<<"调用了两个参数的"<<endl;};private:_Ty m_data;_Last m_data2;};
    template<typename _Ty>
    class Test<_Ty>
    {
    public:Test(_Ty data):m_data(data){cout<<"调用单参数的"<<endl;};
    private:_Ty m_data;};
    template<>
    class Test<>
    {
    public:Test(){cout<<"调用无参数的"<<endl;};};
    int main()
    {//	Test<> test; //  不能传入无参}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
    

执行结果和原理分析

  • 到目前为止,我们并未进行可变参类模板的执行结果进行分析. 现在让我们来分析 (这里我们选择两段式来分析,其实都是相同的)
#include <iostream>
using namespace std;// 主模板展开 <4>
template <typename _Ty, typename... OtherArgs>   // 主模板
class Test : private Test<OtherArgs...> //<1> 继承父类
{
public:Test(){};Test(_Ty FirstData, OtherArgs... args) : _data(FirstData), Test<OtherArgs...>(args...){printf("偏特化版本执行了,this = %p,sizeof...(Others)=%d\n", this, sizeof...(args));}; //<2> 调用父类来处理args 参数
private:_Ty _data;
};template<typename _Ty>
class Test<_Ty>
{
public:Test(_Ty data):m_data(data){cout<<"调用单参数的"<<endl;};
private:_Ty m_data;	
};int main()
{Test<int,float,string>(100,2.5f,"helloWorld");}
123456789101112131415161718192021222324252627282930313233343536

执行结果:

	调用单参数的偏特化版本执行了,this = 0000004f249ff800,sizeof...(Others)=1偏特化版本执行了,this = 0000004f249ff800,sizeof...(Others)=212345
  • 可见 是单参数的 构造函数 先执行,再是双参数,最后是三个参数的被调用

这里贴一张图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如何获取参数包中的值

在之前的基础上 提供接口 之后的代码

// todo  可变参数类模板参数包的展开
// 继承+模板特化
#include <iostream>
using namespace std;
// 主模板
template <typename... Args>
class Test
{
};// 特化模板中展开
template <typename _Ty, typename... Args>
class Test<_Ty, Args...> : private Test<Args...> // 继承父类
{
public:Test(){};Test(_Ty data, Args... args) : _data(data), Test<Args...>(args...){printf("偏特化版本执行了,this = %p,sizeof...(Others)=%d\n", this, sizeof...(args));}; // 调用父类来处理args 参数// 类外访问数据提供接口_Ty &head() { return _data; }; // 返回参数包中第一个参数Test<_Ty, Args...> &tail1() { return *this; }; // 返回本类对象.()  通过类对象能再次调用head(调用当前的第一个元素)Test<Args...> &tail2() { return *this; };      // 返回父类对象.  通过类对象能再次调用head(不断获取剩下参数包的第一个元素)private:_Ty _data;
};int main(int argc, const char **argv)
{Test<int, float, string> one(2, 2.5f, "helloWorld");// 访问第一个数据cout << "firstArg:: " << one.head() << endl;cout << "firstArg2:: " << one.tail1().head() << endl;// 访问第二个数据cout << "secondArg:: " << one.tail2().head() << endl;cout << "secondArg:: " << one.tail2().tail1().head() << endl;// 访问第三个数据cout << "ThirdArg:: " << one.tail2().tail2().head() << endl;
}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647

结合上面两张图 , 我想大家 唯一有疑惑的地方可能就是下面这个

Test<Args...> &tail2() { return *this; };      // 返回父类对象.
1

其实这里是父类指针(引用) 指向子类对象.

比如:

#include<iostream>
using namespace std;
class F 
{
public:int f=100;
};
class S:public F
{
public:int s=200;F Fr;F&retF(){return *this; //这里也是父类引用指向子类对象}
};int main()
{S s;F *Fr =&s;     //父类指针指向子类对象cout<<Fr->f<<endl; //父类指针可以引用到父类成员cout<<s.retF().f;  //这种方式也能调用到父类的成员. 且 s.retF() 返回的是父类地址
}
12345678910111213141516171819202122232425262728

所以 子类对象(one).tail2() 其实 返回的是 父类的地址

组合+递归形式 实现

之前的是用递归+继承的方式实现的, 现在介绍一下组合+递归的方式

#include <iostream>
using namespace std;
using std::string;
// todo 使用递归方式展开
// 主模板
template <typename... Args>
class Tup
{
};// 在特化版本中展开template <typename _Ty, class... Args>
class Tup<_Ty, Args...>
{
public:Tup() = default;Tup(_Ty Firstdata, Args... OtherArgs) : data(Firstdata), args(OtherArgs...){printf("偏特化版本执行了,this = %p,sizeof...(Others)=%d\n", this, sizeof...(args));}; // args 是Tup<Args...> 类型的,所以这里是在递归调用_Ty &head() { return data; }; // 返回参数包第一个数据Tup<Args...> &tail2() // 返回下一个的递归深度的 类对象{return args;// return *this; // 由于不是继承的关系(不存在父类指针(引用)指向子类对象),这里没法用this 返回下一个递归深度的对象,报错:返回临时引用};Tup<_Ty, Args...> &tail1() // 返回这个递归深度的 类对象{// return args; 地址都是一样的return *this; // 返回本类的对象是可以的}protected:_Ty data;Tup<Args...> args; // 下一个递归深度的类对象
};template <>
class Tup<> // 递归终止函数.  //三段式必须含有空的特化类模板
{
public:Tup(){cout << "递归终止函数被调用" << endl;}
};
template <typename _Ty>
class Tup<_Ty> // 含有一个参数的 递归终止函数.
{
public:Tup(_Ty Firstdata) : data(Firstdata){cout << "含有一个参数的递归终止函数被调用" << endl;}; // args 是Tup<Args...> 类型的,所以这里是在递归调用
private:_Ty data;public:_Ty &head() { return data; };
};
void testArgsFirst()
{Tup<int, float, string> one(2, 2.5f, "helloWorld");// 访问第一个数据cout << "firstArg:: " << one.head() << endl;cout << "firstArg2:: " << one.tail1().head() << endl;// 访问第二个数据cout << "secondArg:: " << one.tail2().head() << endl;cout << "secondArg2:: " << one.tail2().tail1().head() << endl;// 访问第三个数据cout << "ThirdArg:: " << one.tail2().tail2().head() << endl;
}int main()
{testArgsFirst();
}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283

这里贴一张图.

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 这个很好理解就是简单的递归


泛型的应用

4 可变参数模版实现泛化的delegate

C++中没有类似C#的委托,我们可以借助可变模版参数来实现一个。C#中的委托的基本用法是这样的:

delegate int AggregateDelegate(int x, int y);//声明委托类型int Add(int x, int y){return x+y;}
int Sub(int x, int y){return x-y;}AggregateDelegate add = Add;
add(1,2);//调用委托对象求和
AggregateDelegate sub = Sub;
sub(2,1);// 调用委托对象相减
123456789

C#中的委托的使用需要先定义一个委托类型,这个委托类型不能泛化,即委托类型一旦声明之后就不能再用来接受其它类型的函数了,比如这样用:

int Fun(int x, int y, int z){return x+y+z;}
int Fun1(string s, string r){return s.Length+r.Length; }
AggregateDelegate fun = Fun; //编译报错,只能赋值相同类型的函数
AggregateDelegate fun1 = Fun1;//编译报错,参数类型不匹配
1234

这里不能泛化的原因是声明委托类型的时候就限定了参数类型和个数,在C++11里不存在这个问题了,因为有了可变模版参数,它就代表了任意类型和个数的参数了,下面让我们来看一下如何实现一个功能更加泛化的C++版本的委托(这里为了简单起见只处理成员函数以及普通函数的情况,并且忽略const、volatile和const volatile成员函数的处理)。

// 一 实现类内函数万能调用的委托
// 1 创建委托类.其中T代表对象类型,R代表函数返回值类型,Args代表函数的任意形参列表的类型
template <class T, class R, typename... Args>
class  MyDelegate
{
public:MyDelegate(T* t, R  (T::*f)(Args...) ):m_t(t),m_f(f) {}R operator()(Args&&... args){return (m_t->*m_f)(std::forward<Args>(args) ...);}
private:T* m_t;                 // 对象指针赋值R  (T::*m_f)(Args...);  // 函数指针赋值,该类实现类似std::thread,例如std::thread thAllOutMsg(&HandleMsg::OutAllMsg, string(tbName));
};// 2 创建万能委托,是一个函数模板,内部调用类模板实现。
template <class T, class R, typename... Args>
MyDelegate<T, R, Args...> CreateDelegate(T* t, R (T::*f)(Args...))//R (T::*f)(Args...)实际上就是函数指针的写法,例如void (A::*f)(int),f并无要求,符合标准命名规范即可,例如改成func也可以。
{return MyDelegate<T, R, Args...>(t, f);
}// 二 重载非类调用的委托,可以实现调用普通函数(对一去掉类类型T即可),但不支持lambda,可以自己进行重载实现
template <class R, typename... Args>
class  MyDelegate1
{
public:MyDelegate1(R  (*f)(Args...) ):m_f(f) {}R operator()(Args&&... args){return (*m_f)(std::forward<Args>(args) ...);}
private:R  (*m_f)(Args...);  // 函数指针赋值
};// 2 创建万能委托,是一个函数模板,内部调用类模板实现。
template <class R, typename... Args>
MyDelegate1<R, Args...> CreateDelegate(R (*f)(Args...))
{return MyDelegate1<R, Args...>(f);
}// 类内函数
struct A
{void Fun(int i){cout<<i<<endl;}void Fun1(int i, double j){cout<<i+j<<endl;}
};// 普通函数
void aa(int a){cout<<a<<endl;    
}
int bb(int a, double b){cout<<a+b<<endl;
}void test18()
{// 1 测试类内函数调用A a;auto d = CreateDelegate(&a, &A::Fun);   //创建委托d(1);       //调用委托,将输出1auto d1 = CreateDelegate(&a, &A::Fun1);d1(1, 2.5); //调用委托,将输出3.5// 2 测试普通函数调用A a2;auto d2 = CreateDelegate(aa);d2(10);     //调用委托,将输出10A a3;auto d3 = CreateDelegate(bb);d3(5, 10.5);//调用委托,将输出15.5
}

MyDelegate实现的关键是内部定义了一个能接受任意类型和个数参数的“万能函数”:R (T::*m_f)(Args…),正是由于可变模版参数的特性,所以我们才能够让这个m_f接受任意参数。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

5 总结

使用可变模版参数的这些技巧相信读者看了会有耳目一新之感,使用可变模版参数的关键是如何展开参数包,展开参数包的过程是很精妙的,体现了泛化之美、递归之美,正是因为它具有神奇的“魔力”,所以我们可以更泛化的去处理问题,比如用它来消除重复的模版定义,用它来定义一个能接受任意参数的“万能函数”等。其实,可变模版参数的作用远不止文中列举的那些作用,它还可以和其它C++11特性结合起来,比如type_traits、std::tuple等特性,发挥更加强大的威力,关于这些特性请参考以下博主的其它文章。

参考文章:C++11可变模版参数的妙用–泛化之美。

折叠表达式

文章目录
  • 折叠表达式
    • 一元左折,一元右折
    • 二元左折,二元右折
    • 可变参表达式
    • 简化打印参数

一元左折,一元右折

namespace detail1
{// 一元左折template <typename... T>auto add_val(T... args){return (... + args);}template <typename... T>auto add_val2(T... args){return (... - args);}// 一元右折template <typename... T>auto add_val3(T... args){return (args + ...);}template <typename... T>auto add_val4(T... args){return (args - ...);}}
1234567891011121314151617181920212223242526cout << "一元左折" << endl;// 一元左折 (unary leftfold)//  格式:(... 运算符 一包参数)cout << detail1::add_val(10, 20, 30, 40); // (((10+20)+30)+40)endl(cout);cout << detail1::add_val2(10, 20, 30, 40); //(((10-20) -30)-40)endl(cout);cout << "一元右折" << endl;// 一元右折 (unary leftfold)//  格式:(  一包参数  运算符    ...)cout << detail1::add_val3(10, 20, 30, 40); // (((40+30)+20)+10)endl(cout);cout << detail1::add_val4(10, 20, 30, 40); // (((40-30)-20)-10)endl(cout);
123456789101112131415

二元左折,二元右折

namespace detail2
{// 二元左折template <typename... T>auto add_val(T... args){return (100 + ... + args);}template <typename... T>auto add_val2(T... args){return (100 - ... - args);}// 一元右折template <typename... T>auto add_val3(T... args){return (args + ... + 100);}template <typename... T>auto add_val4(T... args){return (args - ... - 100);}template <typename... T>void add_val5(T... args){(cout << ... << args);}
}
123456789101112131415161718192021222324252627282930
cout << "二元左折" << endl;// 二元左折//  格式:( init 运算符 ... 运算符 一包参数)// init 表示一个初始的东西,它可能是一个值,也可能是个其他东西。cout << detail2::add_val(10, 20, 30, 40); //((((100+10) +20)+30)+40)endl(cout);cout << detail2::add_val2(10, 20, 30, 40); // ((((100 - 10) - 20) - 30) - 40)endl(cout);cout << "二元右折" << endl;// 二元右折 (unary leftfold)//  格式:(  一包参数  运算符    ... 运算符  init)// init 表示一个初始的东西,它可能是一个值,也可能是个其他东西。// 当init 是一个值时cout << detail2::add_val3(10, 20, 30, 40); // ((((40+30)+20)+10)+100)endl(cout);cout << detail2::add_val4(10, 20, 30, 40); //(10-(20- (30-(40-100))))endl(cout);// 当init 不是一个值,而是一个对象detail2::add_val5(10, 20, 30, 40, " casdcsad ", "sdvsdvsdv"); // 10203040 casdcsad sdvsdvsdv *// cout<<10<<20    cout 是对象 也就是init ,<< 是运算符 ,cout<<10 返回的是cout 对象
1234567891011121314151617181920212223

可变参表达式

namespace detail3
{template <typename... Types>void print(Types const &...args){(std::cout << ... << args) << endl;}// todo  对参数包中所有的参数进行计算,将参数包中的所有的参数都翻倍,然后将结果传给 print()template <typename... T>void printDoubled(T const &...args){print(args + args...);}// todo 对参数包中的每一个参数+1template <typename... T>void addOne(T const &...args){// print(args + 1...); // ERROR: 1... is a literal with too many decimal points// print(args + 1 ...);  // OKprint((args + 1)...); // OK}
}
1234567891011121314151617181920212223
// 调用detail3::printDoubled(7.5, std::string("hello"), std::complex<float>(4, 2));// 等效于调用// print(7.5+7.5,//       std::string("hello") + std::string("hello"),//       std::complex<float>(4,2) + std::complex<float>(4,2));detail3::addOne(11, 10, 12, 14, 15);
12345678

简化打印参数

同样也可以用该方法简化可变参数模板来打印参数:

template<typename... Types>
void print (Types const&... args)
{(std::cout << ... << args) << '\n';
}
12345

如果需要在元素间添加空格,需要额外的类模板:

// basics/addspace.hpptemplate<typename T>
class AddSpace
{
private:T const& ref; // refer to argument passed in constructor
public:AddSpace(T const& r): ref(r) {}friend std::ostream& operator<< (std::ostream& os, AddSpace<T> s) {return os << s.ref << ' '; // output passed argument add a space}
};template<typename... Args>
void print (Args... args){(std::cout << ... << AddSpace(args) ) << '\n';
}
12345678910111213141516171819

此例子中,AddSpace(args)使用类模板实参推导,有AddSpace<Args>(args)的作用,对于每一个实参创建一个AddSpace的实例。

820#_2)

  • 二元左折,二元右折
  • 可变参表达式
  • 简化打印参数

一元左折,一元右折

namespace detail1
{// 一元左折template <typename... T>auto add_val(T... args){return (... + args);}template <typename... T>auto add_val2(T... args){return (... - args);}// 一元右折template <typename... T>auto add_val3(T... args){return (args + ...);}template <typename... T>auto add_val4(T... args){return (args - ...);}}
1234567891011121314151617181920212223242526cout << "一元左折" << endl;// 一元左折 (unary leftfold)//  格式:(... 运算符 一包参数)cout << detail1::add_val(10, 20, 30, 40); // (((10+20)+30)+40)endl(cout);cout << detail1::add_val2(10, 20, 30, 40); //(((10-20) -30)-40)endl(cout);cout << "一元右折" << endl;// 一元右折 (unary leftfold)//  格式:(  一包参数  运算符    ...)cout << detail1::add_val3(10, 20, 30, 40); // (((40+30)+20)+10)endl(cout);cout << detail1::add_val4(10, 20, 30, 40); // (((40-30)-20)-10)endl(cout);
123456789101112131415

二元左折,二元右折

namespace detail2
{// 二元左折template <typename... T>auto add_val(T... args){return (100 + ... + args);}template <typename... T>auto add_val2(T... args){return (100 - ... - args);}// 一元右折template <typename... T>auto add_val3(T... args){return (args + ... + 100);}template <typename... T>auto add_val4(T... args){return (args - ... - 100);}template <typename... T>void add_val5(T... args){(cout << ... << args);}
}
123456789101112131415161718192021222324252627282930
cout << "二元左折" << endl;// 二元左折//  格式:( init 运算符 ... 运算符 一包参数)// init 表示一个初始的东西,它可能是一个值,也可能是个其他东西。cout << detail2::add_val(10, 20, 30, 40); //((((100+10) +20)+30)+40)endl(cout);cout << detail2::add_val2(10, 20, 30, 40); // ((((100 - 10) - 20) - 30) - 40)endl(cout);cout << "二元右折" << endl;// 二元右折 (unary leftfold)//  格式:(  一包参数  运算符    ... 运算符  init)// init 表示一个初始的东西,它可能是一个值,也可能是个其他东西。// 当init 是一个值时cout << detail2::add_val3(10, 20, 30, 40); // ((((40+30)+20)+10)+100)endl(cout);cout << detail2::add_val4(10, 20, 30, 40); //(10-(20- (30-(40-100))))endl(cout);// 当init 不是一个值,而是一个对象detail2::add_val5(10, 20, 30, 40, " casdcsad ", "sdvsdvsdv"); // 10203040 casdcsad sdvsdvsdv *// cout<<10<<20    cout 是对象 也就是init ,<< 是运算符 ,cout<<10 返回的是cout 对象
1234567891011121314151617181920212223

可变参表达式

namespace detail3
{template <typename... Types>void print(Types const &...args){(std::cout << ... << args) << endl;}// todo  对参数包中所有的参数进行计算,将参数包中的所有的参数都翻倍,然后将结果传给 print()template <typename... T>void printDoubled(T const &...args){print(args + args...);}// todo 对参数包中的每一个参数+1template <typename... T>void addOne(T const &...args){// print(args + 1...); // ERROR: 1... is a literal with too many decimal points// print(args + 1 ...);  // OKprint((args + 1)...); // OK}
}
1234567891011121314151617181920212223
// 调用detail3::printDoubled(7.5, std::string("hello"), std::complex<float>(4, 2));// 等效于调用// print(7.5+7.5,//       std::string("hello") + std::string("hello"),//       std::complex<float>(4,2) + std::complex<float>(4,2));detail3::addOne(11, 10, 12, 14, 15);
12345678

简化打印参数

同样也可以用该方法简化可变参数模板来打印参数:

template<typename... Types>
void print (Types const&... args)
{(std::cout << ... << args) << '\n';
}
12345

如果需要在元素间添加空格,需要额外的类模板:

// basics/addspace.hpptemplate<typename T>
class AddSpace
{
private:T const& ref; // refer to argument passed in constructor
public:AddSpace(T const& r): ref(r) {}friend std::ostream& operator<< (std::ostream& os, AddSpace<T> s) {return os << s.ref << ' '; // output passed argument add a space}
};template<typename... Args>
void print (Args... args){(std::cout << ... << AddSpace(args) ) << '\n';
}
12345678910111213141516171819

此例子中,AddSpace(args)使用类模板实参推导,有AddSpace<Args>(args)的作用,对于每一个实参创建一个AddSpace的实例。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/182479.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

智能工厂架构

引:https://www.bilibili.com/video/BV1Vs4y167Kx/?spm_id_from=333.788&vd_source=297c866c71fa77b161812ad631ea2c25 智能工厂框架 智能工厂五层系统框架 MES 数据共享 <

Kafka(二)消息系统设计

文章目录 前言整体设计时序图时序图解释 最后 前言 当多个系统之间通过Kafka来解耦时&#xff0c;在系统设计初期&#xff0c;基本的要求都是相似的&#xff0c;只不过是消费消息时的业务逻辑可能不同。 本文以业务系统和邮件系统解耦作为示例。业务系统需要发送邮件时&#…

SQL左连接实战案例

要求&#xff1a;用表df1和表df2的数据&#xff0c;得到df3 一、创建表 CREATE TABLE df1 (姓名 varchar(255) DEFAULT NULL,年龄 int DEFAULT NULL,部门 varchar(255) DEFAULT NULL,id int DEFAULT NULL );CREATE TABLE df2 (部门 varchar(255) DEFAULT NULL,年龄 int DEFAU…

API接口测试工具的功能及重要性

在现代软件开发中&#xff0c;API(Application Programming Interface)接口的测试至关重要。API接口是不同软件组件之间的桥梁&#xff0c;通过它们实现数据传输和功能交互。API接口测试工具是一类专门用于验证和测试这些接口的软件工具。本文将探讨API接口测试工具的定义、功能…

【高德地图API】JS高德地图API实现多边形绘画,高德获取多边形提交数据

目录 前言效果实现引入js 在项目中使用效果图引入htmlCSS具体实现JS调用说明添加的时候修改的时候判断是否在范围内 java绘画和判断是否在范围内pom.xml依赖引入import引入实现 前言 高德地图官方API&#xff1a;https://lbs.amap.com/demo/javascript-api/example/overlayers…

HTTPS的加密方式超详细解读

在了解https的加密方式之前&#xff0c;我们需要先行了解两个特别经典的传统加密方式&#xff1a; 1、对称加密 1.1、定义 需要对加密和解密使用相同密钥的加密算法。所谓对称&#xff0c;就是采用这种加密方法的双方使用方式用同样的密钥进行加密和解密。密钥是控制加密及解…

SPSS多元方差分析

前言&#xff1a; 本专栏参考教材为《SPSS22.0从入门到精通》&#xff0c;由于软件版本原因&#xff0c;部分内容有所改变&#xff0c;为适应软件版本的变化&#xff0c;特此创作此专栏便于大家学习。本专栏使用软件为&#xff1a;SPSS25.0 本专栏所有的数据文件请点击此链接下…

独立键盘接口设计(Keil+Proteus)

前言 软件的操作参考这篇博客。 LED数码管的静态显示与动态显示&#xff08;KeilProteus&#xff09;-CSDN博客https://blog.csdn.net/weixin_64066303/article/details/134101256?spm1001.2014.3001.5501实验&#xff1a;用4个独立按键控制8个LED指示灯。 按下k1键&#x…

Microsoft Edge不能工作了,可能原因不少,那么如何修复呢

Microsoft Edge打不开或不能加载网页是用户在Windows 10、Android、Mac和iOS设备上的网络浏览器上遇到的许多错误之一。其他Microsoft Edge问题可能包括浏览器窗口和选项卡冻结、网站崩溃、互联网连接错误消息以及丢失Microsoft Edge书签、收藏夹、密码和收藏。 Microsoft Edg…

从零开始制作一个割草机器人

项目背景 为啥要做一个割草机器人呢&#xff1f;&#xff08;个人因素&#xff1a;我梦想就是做一款人形机器人保护人类&#xff0c;解放人类&#xff09; 基础准备&#xff1a;我们公司本身做过高精度&#xff0c;基于高精度的技术扩展到农机自动化驾驶。目前可以实现AB线拖…

【深蓝学院】手写VIO第8章--相机与IMU时间戳同步--作业

0. 题目 1. T1 逆深度参数化时的特征匀速模型的重投影误差 参考常鑫助教的答案&#xff1a;思路是将i时刻的观测投到world系&#xff0c;再用j时刻pose和外参投到j时刻camera坐标系下&#xff0c;归一化得到预测的二维坐标&#xff08;这里忽略了camera的内参&#xff0c;逆深…

51单片机的篮球计分器液晶LCD1602显示( proteus仿真+程序+原理图+PCB+设计报告+讲解视频)

51单片机的篮球计分器液晶LCD1602显示 &#x1f4d1;1.主要功能&#xff1a;&#x1f4d1;讲解视频&#xff1a;&#x1f4d1;2.仿真&#x1f4d1;3. 程序代码&#x1f4d1;4. 原理图&#x1f4d1;5. PCB图&#x1f4d1;6. 设计报告&#x1f4d1;7. 设计资料内容清单&&…

【数据结构】——顺序表(增删查改)

目录 前言&#xff1a; 顺序表&#xff1a; 1、概念及分类 1.1顺序表分类 静态顺序表 动态顺序表 2、接口实现 2.1功能要求 2.2功能实现 &#x1f4a1;初始化顺序表 &#x1f4a1;销毁顺序表 &#x1f4a1;顺序表尾插入 &#x1f4a1;检查是否扩容 &#x1f4a1;…

【二进制转换和与其有关的操作符详解】

文章目录 1.二进制与进制转换2. 2进制转8、10、16进制2.1 2进制转10进制2.2 2进制转8进制2.3 2进制转16进制 3. 8、10、16进制转2进制3.1 10进制转2进制3.2 8进制转2进制3.3 16进制转2进制 4.原码、反码、补码5.移位操作符&#xff08;<< >>&#xff09;5.1左移操作…

【IDEA】在工具栏设置快速创建包和类的图表

页面效果&#xff1a; 操作步骤&#xff1a; 设置 --> 外观与行为 --> 菜单与工具栏 --> 点击 主工具栏 --> 点击 ---- --> 点击 号 --> 添加操作 主菜单 --> 文件 --> 文件打开操作 --> 打开项目操作 --> 新建 --> 往下找 找到 clas…

超声波俱乐部分享:百度世界大会点燃AI创业者新希望

10月22日&#xff0c;2023年第十三期超声波俱乐部内部分享会在北京望京举行。本期的主题是&#xff1a;百度世界大会点燃AI创业者新希望。 到场的嘉宾有&#xff1a;超声波创始人杨子超&#xff0c;超声波联合创始人、和牛商业创始人刘思雨&#xff0c;中国国际经济交流中心研…

开启AWS的ubuntu服务器的root用户登录权限

设置root用户密码 输入以下命令修改root用户密码 sudo passwd root输入以下命令切换到root用户 su root仅允许root用户用密码登录 输入以下命令编辑ssh配置文件 vi /etc/ssh/sshd_config新增以下配置允许root用户登录 PermitRootLogin yes把PasswordAuthentication修改为…

成员变量为动态数据时不可轻易使用

问题描述 业务验收阶段&#xff0c;遇到了一个由于成员变量导致的线程问题 有一个kafka切面&#xff0c;用来处理某些功能在调用前后的发送消息&#xff0c;资产类型type是成员变量定义&#xff1b; 资产1类型推送消息是以zichan1为节点&#xff1b;资产2类型推送消息是以zi…

2023-11-06今日最大收获:坑爹的 JpaRepository!

1.坑爹的 JpaRepository&#xff01; org.springframework.dao.InvalidDataAccessResourceUsageException: could not extract ResultSet; SQL [n/a]; nested exception is org.hibernate.exception.SQLGrammarException: could not extract ResultSet 2023-11-06 18:38:53.12…

报错Could not resolve placeholder ‘driver‘ in value “${driver}“

这是我的报错&#xff1a; 原因是我的applicationContext.xml文件加载properties文件径错误&#xff1a; 应该把路径改成这样就可以了&#xff1a;