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

news2024/11/25 16:00:29

文章目录

  • 函数模板(第一部分)
      • 定义函数模板
      • 使用函数模板
            • 样例
      • 两阶段翻译 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>   13
    Function(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() unknown

  undeclared(t);       // second-phase compile-time error if undeclared(T) unknown

  static_assert(sizeof(int) > 10,"int too small");      // first-phase compile-time error

  static_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 : b
  return 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
    /// @return
    cout << 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
    /// @return
    cout << test02::add<float, int>(22.5, 35) << endl;
    cout << test02::add<int, int>(22, 35) << endl;
    /// @brief 将返回值声明为两个模板参数的公共类型
    /// @param argc
    /// @param argv
    /// @return
    cout << test03::add<float, int>(22.5, 35) << endl;
    cout << test03::add<int, int>(22, 35) << endl;
    /// @brief 这里使用_t后缀让我们不用写typename和::type
    /// @param argc
    /// @param argv
    /// @return
    cout << 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
    /// @return
    auto 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 1

template <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 函数模板的重载
    /// @return
    cout << 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 element
  void pop();               // pop element
  T const &top() const;     // return top element
  bool empty() const {      // return whether the stack is empty
    return 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 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();
}
12345678910111213
注意
  • 在类声明内的构造函数、拷贝构造函数、析构函数、赋值等用到类名字的地方,可以将Stack<T>简写为Stack,例如:
template<typename T>
class Stack {
  ...
  Stack (Stack const&);                           // copy constructor
  Stack& 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; // 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();
        }
        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 ints
    detail::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();
}
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 0
    template <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 ;
    // 在声明中指定缺省值
    // 声明1
    template <typename U, typename T, typename Z, typename Y, typename X = int>
    class TC;
    // 声明2
    template <typename U, typename T, typename Z, typename Y = double, typename X> // 这里的X 已经被指定默认参数了
    class TC;
    // 声明3
    template <typename U, typename T = U *, typename Z = const int *, typename Y, typename X = int> // 这里的X Y已经被指定默认参数了
    class TC;
    // 声明4
    template <typename U, typename T = double *, typename Z, typename Y, typename X = int> // 这里的X Y Z已经被指定默认参数了
    class TC;
    // 声明5
    template <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 0
    class 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;
}
#endif

1234567891011121314151617181920212223242526272829303132333435

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*,依赖于T
    template <typename T> //(3)
    T g_tmp<T *>{10};

    // 变量模板的偏特化,从参数范围上的偏特化,必须要求是T*,依赖于T
    template <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 declaration
    template <typename T>
    constexpr T PI<T>::pi;

    int main(int, char **)
    {
        std::cout << PI<int>::pi << std::endl;
        // 3
        std::cout << PI<double>::pi << std::endl;
        // 3.14159
        return 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> // 我们写作默认int
    T pi{};                     // 初始化列表 为0
    int main()
    {
        std::cout << precision(16);
        pi<int> = 20;                      // pi<>=20;效果一样
        std::cout << pi<int> << std::endl; // 20
        pi<double> = 3.14159265358;
        std::cout << pi<double> << std::endl; // 3.14159265358
        std::cout << pi<float> << std::endl;  // 0
        return 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>
    auto
    func(T &a)
    {
        std::cout << typeid(a).name() << " ";
        if (std::is_array_v<T>)
        {
            std::cout << "是数组" << std::endl;
        }
        else
            std::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;
        }
        else
            std::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;
        }
        else
            std::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;
        }
        else
            std::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 initialized

    template <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 double
    std::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 char

    detail3::arr<10>[0] = 42;                                 // sets first element of global arr
    for (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*,依赖于T
    template <typename T> //(3)
    T g_tmp<T *>{10};

    // 变量模板的偏特化,从参数范围上的偏特化,必须要求是T*,依赖于T
    template <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();  //5



bobj2.callBAF();  //5

  • 让类型模板参数成为友元类
    • C++11新标准中引入:如果传递进来的 类型模板参数 是一个类类型,则这个类类型可以成为当前类模板的友元类。
template <typename T>
class A2
{
	friend T;
private:
	int data;
};
 
class CF
{
public:
	void callCFAF()
	{
		A2<CF> aobj1;    -- 让CF类成为了A2
		aobj1.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();
  • 输出:
12
 15

让某个类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类成为了A2
		aobj1.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类模板中定义的友元函数func2
 
Men<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 argument
            return;
        }
        
        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)=2

12345
  • 可见 是单参数的 构造函数 先执行,再是双参数,最后是三个参数的被调用

这里贴一张图:

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

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

如何获取参数包中的值

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

// 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);       //调用委托,将输出1
    auto d1 = CreateDelegate(&a, &A::Fun1);
    d1(1, 2.5); //调用委托,将输出3.5

    // 2 测试普通函数调用
    A a2;
    auto d2 = CreateDelegate(aa);
    d2(10);     //调用委托,将输出10
    A 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 - ...);
    }

}
1234567891011121314151617181920212223242526
   cout << "一元左折" << 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 对参数包中的每一个参数+1
    template <typename... T>
    void addOne(T const &...args)
    {
        // print(args + 1...); // ERROR: 1... is a literal with too many decimal points
        // print(args + 1 ...);  // OK
        print((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.hpp

template<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 - ...);
    }

}
1234567891011121314151617181920212223242526
   cout << "一元左折" << 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 对参数包中的每一个参数+1
    template <typename... T>
    void addOne(T const &...args)
    {
        // print(args + 1...); // ERROR: 1... is a literal with too many decimal points
        // print(args + 1 ...);  // OK
        print((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.hpp

template<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.coloradmin.cn/o/1178037.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

偏序关系用分治优化建图:ARC165F

https://atcoder.jp/contests/arc165/tasks/arc165_f 首先可以建图&#xff0c;然后变成求字典序最小的的拓扑排序 然后发现这样复杂度会炸&#xff0c;观察连边的条件是什么&#xff1a; l i < l j l_i<l_j li​<lj​ r i < r j r_i<r_j ri​<rj​ 这是个…

麒麟-v10系统添加字体方法

先找到需要添加的字库文件&#xff0c;一般为TTF文件。 例如&#xff1a;方正粗黑宋简体.ttf 在 /usr/share/fonts 路径下创建一个chines 文件夹 。 * * * 注意以下所有操作涉及到的操作命令&#xff0c;均需ROOT操作。 mkdir /usr/share/fonts/chines 三&#xff0e;将需…

Single Image Haze Removal Using Dark Channel Prior(暗通道先验)

去雾算法都会依赖于很强的先验以及假设&#xff0c;并结合相应的物理模型&#xff0c;完成去雾过程。本文作者何凯明及其团队通过大量的无雾图像和有雾图像&#xff0c;归纳总结出无雾图像在其对应的暗通道图像上具有极低的强度值&#xff08;趋近于0&#xff09;&#xff0c;并…

虚拟机没有桥接模式--物理机WiFi不见了--注册表修复

我们知道虚拟机有三种模式&#xff1a; vmnet0 桥接模式&#xff1b;vmnet1 仅主机模式&#xff1b;vmnet8 NAT模式 我自己以前一直用的NAT模式&#xff0c;今天突然要用到桥接模式&#xff0c;发现无法切换... 我下面这个是后面弄好了的&#xff0c;最开始是没有显示桥接模式…

运放电压跟随器为什么要加电阻

这个是运放构成的电压跟随器&#xff0c;他的特点是输出电压等于输入电压&#xff0c;它常常用来对信号进行隔离&#xff0c;缓冲和提高带载能力。 有时候我们还会在电压跟随器上加这两个电阻&#xff0c;其中R1主要是起保护作用&#xff0c;Rf主要是为了消除偏置电流对输出电压…

stable-diffusion-webui安装Wav2Lip

常见错误 1.错误&#xff1a;Torch is not able to use GPU; add --skip-torch-cuda-test to COMMANDLINE_ARGS variable to disable this check 修改代码&#xff1a; launch_utils.py 删除三个地方&#xff1a;

LangChain+LLM实战---文本分块(Chunking)方法

RAG是一个考验技术的工作 基于大模型的企业应用中很大一部分需求就是RAG——检索增强生成。 这个流程依然无法描述RAG的复杂性 RAG涉及的内容其实广泛&#xff0c;包括Embedding、分词分块、检索召回&#xff08;相似度匹配&#xff09;、chat系统、ReAct和Prompt优化等&…

Optional——优雅判空

初始化 Optional提供了三个初始化方法&#xff1a; SpringBootTest public class OptionalTest {Testpublic void testOptional() {Optional.empty();Optional.ofNullable(null);Optional.of(null);} }empty返回一个空的Optional对象。 of遇到空会报错&#xff0c;但是使用Op…

Python | 安装、环境配置及包的安装

Python | 安装、环境配置及包的安装 一、前言二、python安装及编辑器配置2.1 python安装2.2 python调试2.3 python编辑器 | PyCharm2.3.1 PyCharm下载2.3.2 PyCharm安装2.3.3 PyCharm启动界面2.3.4 PyCharm初步设置2.3.5 PyCharm环境配置(含Python Interpreter配置)2.3.5.1 New…

2003-2022年飞机航线信息数据

2003-2022年飞机航线信息数据 时间&#xff1a;2003-2022年指标&#xff1a;起点城市、起点城市所属地级市、起点城市所属省份、起点机场、终点城市、终点城市所属地级市、终点城市所属省份、终点机场、航空公司、航班、机型、出发时间、到达时间、准点率、班次_周一、班次_周…

pip安装apex报错ERROR: Could not build wheels for cryptacular.......

问题&#xff1a;在训练模型的时候需要安装apex包&#xff0c;遂即使用以下命令 pip install apex但是报错了&#xff0c;报错信息如下&#xff1a; WARNING: Building wheel for cryptacular failed: [Errno 2] No such file or directory: C:\\Users\\XXX\\AppData\\Local\…

Corel VideoStudio 会声会影2024剪辑中间的视频怎么删 剪辑中音乐太长怎么办

我很喜欢视频剪辑软件Corel VideoStudio 会声会影2024&#xff0c;因为它使用起来很有趣。它很容易使用&#xff0c;但仍然给你很多功能和力量。视频剪辑软件Corel VideoStudio 会声会影2023让我与世界分享我的想法&#xff01;“这个产品的功能非常多&#xff0c;我几乎没有触…

解决找不到msvcp120.dll,无法继续执行代码的办法,msvcp120.dll丢失的解决办法

在使用电脑的过程中出现了“找不到msvcp120.dll,无法继续执行代码”&#xff0c;通常出现这种错误的原因是因为电脑中的msvcp120.dll文件丢失&#xff0c;但是文件丢失就会导致电脑出现软件不能打开的情况&#xff0c;也可能会导致电脑出现其他的问题&#xff0c;所以今天就给大…

【学习草稿】

【数据分析】 1、相关性分析 对变量之间相关关系的分析&#xff0c;即相关性分析。其中比较常用的是线性相关分析&#xff0c;用来衡量它的指标是线性相关系数&#xff0c;又叫皮尔逊相关系数&#xff0c;通常用r表示&#xff0c;取值范围是[-1,1]。 r的绝对值<0.3 ,低度线性…

spring报错 @EnableAsync annotation metadata was not injected

报错 报错 internalAsyncAnnotationProcessor 这个spring内部的后处理器 创建失败&#xff0c;进而导致 EnableAsync 注解元数据没有注入容器 分析问题 查了 博客 是配置类放到原始项目路径下导致的问题。 博主的路径虽然正确&#xff0c;但发现是相似的问题&#xff0c;最…

linux 驱动——将模块编译进内核

文章目录 新增 C 文件修改 Makefile 文件修改 Kconfig 文件模块使能内核启动日志参考 linux 驱动——字符设备驱动 linux 驱动——字符设备驱动(自动生成设备节点文件) linux 驱动——将模块编译进内核 前面两节介绍的驱动都是以模块的形式&#xff0c;需要手动加载&#xff0…

【kubernetes】pod的生命周期

文章目录 1、概述2、pod的生命期3、pod阶段4、容器状态5、容器重启策略6、pod状况6.1 Pod就绪态6.2 Pod就绪态的状态6.3 Pod网络就绪 7、容器探针7.1 检查机制7.2 探测结果7.3 探测类型 8、Pod的终止8.1 强制终止Pod8.2 Pod的垃圾收集 1、概述 pod遵循预定义的生命周期&#x…

matlab中的mapminmax函数初步理解和应用

matlab中的mapminmax函数初步认识 一、mapminmax 顾名思义&#xff1a;映射最大最小 二、语法及举例 2.1 语法1 [Y,PS] mapminmax(X) 将矩阵X映射形成矩阵Y, Y中每行中的最小值对应-1&#xff0c;最大值对应1。PS是一个包含映射信息的结构体。 举例&#xff1a; clc cle…

4.第一个Java程序的讲解—Hello World

本文将写一个程序输出 Hello World &#xff0c;然后逐句讲解 ~ 文章目录 一、输出 Hello World二、代码讲解2.1 package com.goole.demo;2.1.1 .idea、out、src2.1.2 解释 2.2 public class Main2.2.1 解释2.2.2 创建新类 2.3 public static void main(String[] args)2.3.1 解…

测试用例的设计方法(全):判定表驱动分析方法

目录 判定表驱动分析方法 一. 方法简介 二. 实战演习 判定表驱动分析方法 一. 方法简介 1.定义&#xff1a;判定表是分析和表达多逻辑条件下执行不同操作的情况的工具。 2.判定表的优点 能够将复杂的问题按照各种可能的情况全部列举出来&#xff0c;简明并避免遗漏。因此…