C++基础与深度解析 | 模板 | 函数模板 | 类模板与成员函数模板 | concepts | 完美转发 | 模板的其他内容

文章目录

  • 一、函数模板
  • 二、类模板与成员函数模板
  • 三、Concepts(C++20)
  • 四、模板相关内容
    • 1.数值模板参数与模板模板参数
    • 2.别名模板与变长模板
    • 3.包展开与折叠表达式
    • 4.完美转发与lambda表达式模板
    • 5.消除歧义与变量模板

一、函数模板

  在C++中,函数模板是一种允许你编写可以处理多种数据类型的函数的方式。函数模板通过使用模板参数来实现泛型编程,这样同一个函数就可以用不同的数据类型来调用

函数模板不是函数。

使用 template 关键字引入模板

  使用 template<typename T> 或者 template<class T> 来定义一个函数模板。typenameclass 在这里可以互换使用,但通常 typename 更常用于模板参数。

template<typename T> 
void fun(T) 
{//...
}

函数模板的声明与定义

  在一个翻译单元中,函数声明可以包含多次,但函数定义只能包含一次。函数模板的声明和定义通常写在一起。例如:

template<typename T>
void functionTemplate(T param) {// 函数实现
}

这个函数模板 functionTemplate 可以接受任何类型的参数 param

函数模板参数

  函数模板中包含了两对参数:函数形参 / 实参;模板形参 / 实参。

  • 函数形参:函数定义中的参数,如上面例子中的 param
  • 函数实参:调用函数时传递给函数的具体参数值。
  • 模板形参:在模板定义中使用的类型或值的占位符,如 T
  • 模板实参:在调用模板时,提供给模板形参的具体类型或值

函数模板的显式实例化

  在C++中,函数模板的显式实例化是一种告诉编译器创建一个特定函数模板实例的操作。

  • 显式实例化的语法

    显式实例化使用模板函数名后跟尖括号内指定的类型参数来完成。例如,fun<int>(3) 告诉编译器实例化模板函数 fun 并使用 int 作为模板参数,然后调用这个实例化函数并传递整数 3 作为参数。

  • 实例化会使得编译器产生相应的函数(函数模板并非函数,不能调用)

    使用C++ Insights可知:

    image-20240606113537606

  • 编译期的两阶段处理(函数模板的实例化发生在编译期)

    • 模板语法检查

      编译器首先检查模板代码的语法是否正确

    • 模板实例化

      编译器根据提供的模板参数来生成具体的函数代码。

  • 模板必须在实例化时可见–翻译单元的一处定义原则

    模板的定义只在一个翻译单元中出现一次,以避免链接错误。

  • 与内联函数的异同

    虽然函数模板与内联函数都满足翻译单元级别的一次定义原则而非程序级别的一次定义原则,但原因是不同的。

    • 共同点:两者都可以在编译时进行优化,并且都可以在头文件中定义。
    • 不同点:内联函数不涉及类型参数,而函数模板是类型安全的泛型函数。

函数模板的重载

  在C++中,函数模板的重载指的是可以定义多个具有相同名称但模板参数不同的函数模板。当编译器尝试确定哪个函数模板实例与给定的调用匹配时,它会根据传递给函数的实参类型来解析重载。如果存在多个匹配的模板实例,编译器将选择最匹配的一个。

示例:

#include <iostream>template<typename T>
void fun(T input)
{std::cout << input << std::endl;
}template<typename T>
void fun(T* input)
{std::cout << *input << std::endl;
}template<typename T, typename T2>
void fun(T input, T2 input2)
{std::cout << input << std::endl;std::cout << input2 << std::endl;
}int main()
{double x = 3.14;fun<int>(3);fun<double>(&x);
}

编译后的结果为

#include <iostream>template<typename T>
void fun(T input)
{(std::cout << input) << std::endl;
}/* First instantiated from: insights.cpp:25 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
void fun<int>(int input)
{std::cout.operator<<(input).operator<<(std::endl);
}
#endiftemplate<typename T>
void fun(T * input)
{(std::cout << *input) << std::endl;
}/* First instantiated from: insights.cpp:26 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
void fun<double>(double * input)
{std::cout.operator<<(*input).operator<<(std::endl);
}
#endiftemplate<typename T, typename T2>
void fun(T input, T2 input2)
{(std::cout << input) << std::endl;(std::cout << input2) << std::endl;
}int main()
{double x = 3.1400000000000001;fun<int>(3);fun<double>(&x);return 0;
}

模板实参的类型推导:(隐式实例化)

  在C++中,模板实参的类型推导是一个自动确定模板参数类型的过程,推导是基于函数实参(表达式)确定模板实参的过程。如果函数模板在实例化时没有显式指定模板实参,那么系统会尝试进行推导。

模板实参类型推导的基本原则:(与auto类型推导相似)

  • 当函数形参是左值引用或指针

    忽略实参表达式的引用部分,并尝试匹配表达式的类型与形参类型来确定模板实参。

    示例:

    template<typename T>
    void func(T& param) {// ...
    }
    int main() 
    {const int a = 5;func(a); // T& 推导为 const int
    }
    

    image-20240606133737821

  • 当函数形参是万能引用(使用 T&& 声明)时

    模板实参的类型推导将根据实参表达式的值类别(左值或右值)来确定:

    • 如果实参是一个右值,模板实参将被推导为被推导为去掉引用的基本类型
    • 如果实参是一个左值,模板实参将被推导为左值引用类型,这将触发引用折叠规则。

    示例:

    template<typename T>
    void func(T&& param) {// ...
    }
    int main() 
    {const int a = 5;func(a); // T 推导为 const int&,引用折叠指的是const int& && ==> const int&func(10.1); //T 推导为double
    }
    

    image-20240606134324822

  • 当函数形参不包含引用

    模板实参的类型推导将忽略实参表达式的引用部分和顶层 const,并且:

    • 数组和函数类型将转换成相应的指针类型。
    • 其他类型将直接推导为该类型。

    示例:

    template<typename T>
    void func(T param) {// ...
    }void someFunction()
    {
    }int main()
    {int x = 2;const int& y = x;func(x);	//T推导为 int,忽略引用与顶层constconst int* const ptr = &x;func(ptr);	//T推导为const int*,忽略顶层constint arr[] = {1, 2, 3};func(arr); // T 推导为 int*,因为 arr 是数组类型void (*funcPtr)() = someFunction;func(funcPtr); // T 推导为 void (*)(),因为 funcPtr 是函数指针
    }

    image-20240606140314544

模板实参并非总是能够推导得到

  • 如果模板形参与函数形参的类型无关,则编译器可能无法从函数实参推断出模板实参的类型。

    例如:

    template<typename T, typename U>
    U func(T param) {// ...
    }
    
  • 即使相关,也不一定能进行推导,

  • 推导成功也可能存在因歧义而无法使用

    例如:

    template<typename T>
    void func(T param1, T param2) {// ...
    }int main()
    {func(3, 5.0);
    }
    

在无法推导时,编译器会选择使用缺省模板实参,可以为任意位置的模板形参指定缺省模板实参。

注意与函数缺省实参的区别,不需要保证缺省实参右边全部为缺省实参

例如:

template<typename T, typename U = int>
U func(T param) {// ...
}int main()
{func(3);
}

显式指定部分模板实参

  即在调用模板函数时明确指定其中一些模板参数,而让编译器自动推导剩余的参数。

  • 显式指定的模板实参必须从最左边开始,依次指定

    一旦你开始显式指定模板实参,编译器将自动推导剩余未指定的模板参数。如果推导失败,将导致编译错误。

  • 模板形参的声明顺序会影响调用的灵活性

    例如:

    //这种模板形参的声明顺序会编译错误
    template<typename T, typename U>
    U func(T param) {// ...
    }template<typename U, typename T>
    U func(T param) {// ...
    }int main()
    {func<int>(3);
    }
    

    image-20240606143454338

函数模板自动推导时会遇到几种情况

  • 函数形参无法匹配—— SFINAE (替换失败并非错误)

    当模板参数推导失败时,如果是因为类型不匹配导致的,编译器会认为这不是一个错误,而是简单地排除这个模板实例化选项。

  • 模板与非模板同时匹配,匹配等级相同,此时选择非模板的版本

    如果模板函数和非模板函数都可以匹配同一个调用,并且它们的匹配等级相同,那么编译器将选择非模板函数。这是因为在C++中,非模板函数的优先级高于模板函数:

    例如:

    void func(int i) {// ...
    }template<typename T>
    void func(T t) {// ...
    }int main()
    {func(5); // 调用非模板 func(int i),因为匹配等级相同
    }
    
  • 多个模板同时匹配,此时采用偏序关系确定选择”最特殊“的版本

    如果有多个模板实例都可以匹配同一个调用,C++的重载解析规则将根据偏序关系来确定“最特殊”的版本。这通常涉及到模板参数的特化程度,更具体的模板实例会被优先选择:

    例如:

    template<typename T>
    void func(T t) {// ...
    }template<>
    void func<int>(int i) {// ...
    }int main()
    {func(5); // 调用模板特化 func<int>(int i),因为它更具体 
    }
    

函数模板的实例化控制:显式实例化但不调用

  • 显式实例化定义

    显式实例化定义是告诉编译器为特定的模板参数生成一个实例。这通常在模板定义的实现文件中完成。

    template 
    void fun<int>(int);
    或者
    template 
    void fun(int);
    

    image-20240606151909033

  • 显式实例化声明

    显式实例化声明用于告诉编译器存在一个显式实例化的定义。

    extern template 
    void fun<int>(int);
    或者
    extern template 
    void fun(int);
    

    如果引入了显式实例化声明,就不会产生模板实例,减轻了编译器的负担,在链接过程中也不需要将相同的实例删除掉,提升编译与链接速度。

  • 注意一处定义原则(程序级别)

    C++要求每个模板显式实例化在程序中只能有一个定义。这意味着显式实例化定义只能出现在一个编译单元(通常是一个.cpp文件)中。违反这一原则会导致链接错误。

  • 注意实例化过程(显式实例化定义)中的模板形参推导

函数模板的(完全)特化

  C++中的函数模板特化是为特定类型提供特定实现的一种方式。与函数重载不同,特化是为已经存在的模板函数提供针对特定类型的特定实现。本质上函数模板特化就是函数模板实例化。

完全特化语法:

// 模板定义
template<typename T>
void f(T t) {// 通用实现
}// 函数模板的完全特化
template<>
void f<int>(int t) {// int 类型的具体实现
}
  • 并不引入新的(同名)名称,只是为某个模板针对特定模板实参提供优化算法

    特化不会创建新的函数名称。它只是为模板函数提供了一个针对特定参数的定制版本。这意味着特化和非特化版本在函数名上是相同的,只是参数类型不同。

  • 注意与重载的区别

    • 重载:涉及创建多个具有相同名称但参数类型或数量不同的函数。
    • 特化:为模板函数提供针对特定类型的定制实现,不增加新的函数名称。
  • 注意特化过程中的模板形参推导

    特化可以影响模板形参的推导过程。

避免使用函数模板的特化

  • 不参与重载解析,会产生反直觉的效果(重载解析是在函数模板特化之前完成的)

    函数模板特化不参与普通的重载解析过程。这意味着即使存在一个更匹配的非特化版本,编译器也可能选择特化版本,因为特化版本在重载候选中具有更高的优先级。这可能导致一些反直觉的结果。

  • 通常可以用重载代替(函数重载会参与重载解析)

    优先使用函数重载而不是模板特化。函数重载遵循标准的重载解析规则,这使得代码的行为更加可预测和直观。

  • 一些不便于重载的情况:无法建立模板形参与函数形参的关联,可以考虑一下替代方案

    • 使用if constexpr解决

      if constexpr 是C++17引入的一个特性,它允许在编译时根据模板参数的值选择执行不同的代码路径。这可以用来模拟重载的效果

      #include <type_traits>template<typename T>
      void func(T t) {if constexpr (std::is_same_v<T, int>) {// 针对 int 类型的代码} else {// 通用代码}
      }
      
    • 引入“假”函数形参

    • 通过类模板特化解决

      使用类模板特化,然后在类中定义需要重载的函数。这种方法可以将重载的复杂性封装在类内部:

      template<typename T>
      struct MyClass {void func(T t) {// 通用实现}
      };template<>
      struct MyClass<int> {void func(int t) {// 针对 int 类型的实现}
      };
      

函数模板的简化形式(C++20):使用auto定义模板参数类型

  在C++20中,引入了使用auto来定义函数模板参数类型的简化形式。

  • 优势:书写简捷

  • 劣势:在函数内部需要间接获取参数类型信息

image-20240606161910859

二、类模板与成员函数模板

  在C++中,类模板是一种泛型编程工具,允许你创建可以处理多种数据类型的类。

类模板不是类

  • 使用template关键字引入类模板

      使用 template<typename T>template<class T> 来声明一个类模板。typenameclass 在这里可以互换使用,但 typename 更常用于模板参数。

    template<typename T>
    class B {// 包括成员变量、成员函数的实现
    };
    
  • 类模板的声明与定义:翻译单元级别的一处定义原则

      类模板的声明和定义通常写在一起。类模板的定义包括成员变量、成员函数的实现等。

  • 成员函数只有在调用时才会被实例化

      类模板的成员函数只有在被调用时才会被实例化。这意味着编译器会根据成员函数调用时提供的实参来生成具体的函数实现。

    #include <iostream>template<typename T>
    class B {
    public:void fun(T input){std::cout << input << std::endl;}
    };int main()
    {B<int> x;x.fun(3);
    }
    

    经编译器实例化后如下:

    #include <iostream>template<typename T>
    class B
    {public: inline void fun(T input){(std::cout << input) << std::endl;}};/* First instantiated from: insights.cpp:14 */
    #ifdef INSIGHTS_USE_TEMPLATE
    template<>
    class B<int>
    {public: inline void fun(int input){std::cout.operator<<(input).operator<<(std::endl);}// inline constexpr B() noexcept = default;
    };#endifint main()
    {B<int> x;x.fun(3);return 0;
    }
    

    类模板中的成员函数本质上是内联函数

  • 类内类模板名称的简写

    在类模板的成员函数中,可将类模板名称进行简写

    image-20240606171534460

  • 类模板成员函数的定义(类内、类外)

    类模板的成员函数可以在类内定义(内联定义)或类外定义。

    • 类内定义

      成员函数在类模板内部定义时,不需要再次使用 template 关键字,编译器能够从上下文中推断出模板参数。

      template<typename T>
      class B {
      public:void memberFunc() {// 实现}
      };
      
    • 类外定义

      当成员函数在类外部定义时,需要使用模板关键字,并显式指定模板参数。

      template<typename T>
      class B {
      public:void memberFunc();
      };template<typename T>
      void B<T>::memberFunc() {// 实现
      }
      

成员函数模板

  • 类的成员函数模板

    位于类内部的函数模板,同上也可分为类内定义与类外定义

    class B {
    public:template<typename T>void func(T input);
    };template<typename T>
    void B::func(T input)
    {}int main()
    {B x;x.func<int>(3);
    }
    
  • 类模板的成员函数模板

    类模板可以包含成员模板函数,这些函数在类内或类外定义

    template<typename T>
    class B {
    public:template<typename T2>void func(T2 input);
    };template<typename T>
    template<typename T2>
    void B<T>::func(T2 input)
    {}int main()
    {B<int> x;x.func<int>(3);
    }
    

友元函数模板:(很少使用)

  • 可以声明一个函数模板为某个类(模板)的友元
  • C++11 支持声明模板参数为友元
template<typename T>
class B {
public:template<typename T2>friend void func(T2 input);private:int x;
};template<typename T2>
void func(T2 input)
{B<int> tmp1;tmp1.x;B<char> tmp2;tmp2.x;
}int main()
{func<float>(3);
}

类模板的实例化

C++中的类模板实例化与函数模板实例化在概念上是相似的,都涉及到根据提供的模板实参生成具体的类型或函数。

详细内容可参考:https://en.cppreference.com/w/cpp/language/class_template

  • 实例化过程

    类模板的实例化是通过替换模板参数来创建一个具体类的版本。这个过程可以是隐式的,也可以是显式的。

  • 隐式实例化

    当你创建一个类模板的对象或调用其成员函数时,如果模板参数没有明确指定,编译器会自动推导这些参数,从而实例化类或成员函数。

    template<typename T>
    class Box {T item;
    public:Box(T t) : item(t) {}T getItem() const { return item; }
    };int main() {Box<int> myBox(10); // 隐式实例化 Box<int>return 0;
    }
    
  • 显式实例化

    显式地要求编译器实例化类模板的特定版本。这通常在类模板的定义完成后进行

    template class Box<int>; // 显式实例化 Box<int>
    
  • 可以实例化整个类模板或者类模板中的某个成员函数

    • 实例化整个类模板,即为该类创建一个具体的类型版本

      Box<double> anotherBox(5.5); // 实例化 Box<double> 的对象
      
    • 实例化类模板中的某个成员函数,特别是当成员函数是模板时

      template<typename T>
      class MyClass {
      public:template<typename U>U templatedMethod(U u) {return u;}
      };
      int main()
      {// 实例化 MyClass<int> 的 templatedMethod<double>MyClass<int> myObject;double result = myObject.templatedMethod<double>(3.14);   
      }
      

类模板的(完全)特化 / 部分特化

  • 完全特化

    完全特化是指为类模板的特定类型参数提供完全定制的类定义。特化版本与基础版本可以完全不同,不继承或包含基础模板的任何成员。

    // 基础模板类定义
    template<typename T>
    class MyClass {
    public:void func() { /* ... */ }
    };// 完全特化版本
    template<>
    class MyClass<int> {
    public:void func() { /* 完全不同的实现 */ }
    };
    

    MyClass<int>MyClass 的一个完全特化版本,它具有与基础模板完全不同的实现。

  • 部分特化

    部分特化或偏特化是指当类模板接受多个类型参数时,可以为其中一些参数提供特化,而其他参数保持通用。

    // 基础模板类定义,接受两个类型参数
    template<typename T1, typename T2>
    class MyClass {
    public:void func() { /* ... */ }
    };// 部分特化版本,T2 特化为 int
    template<typename T1>
    class MyClass<T1, int> {
    public:void func() { /* 针对 T1 的任意类型和 T2 为 int 的特化实现 */ }
    };
    

    MyClass<T1, int> 是一个部分特化版本,它只针对 T2int 的情况提供了定制的实现,而 T1 可以是任何类型。

类模板的实参推导:(从C++17开始)

  • 基于构造函数的实参推导

    C++17允许编译器根据构造函数的参数来推导类模板的模板参数。如果构造函数的参数能够明确地推导出模板参数的类型,编译器将自动实例化类模板。、

    template<typename T>
    class Wrapper {
    public:T value;Wrapper(T v) : value(v) {}
    };int main() {Wrapper w(42); // C++17 允许从 42 推导 T 为 int
    }
    

    经编译器翻译后如下:

    template<typename T>
    class Wrapper
    {public: T value;inline Wrapper(T v): value(v){}
    };/* First instantiated from: insights.cpp:9 */
    #ifdef INSIGHTS_USE_TEMPLATE
    template<>
    class Wrapper<int>
    {public: int value;inline Wrapper(int v): value{v}{}
    };#endifint main()
    {Wrapper<int> w = Wrapper<int>(42);return 0;
    }template<typename T>
    Wrapper(T v) -> Wrapper<T>;/* First instantiated from: insights.cpp:9 */
    #ifdef INSIGHTS_USE_TEMPLATE
    template<>
    Wrapper(int v) -> Wrapper<int>;
    #endif
    
  • 用户自定义的推导指引

    C++17还引入了自定义的推导指引(Deduction Guide),允许开发者提供构造函数的重载来帮助编译器进行类型推导。推导指引需要在类模板的作用域外部定义,以便编译器能够在整个程序中找到并使用它们。

    template<typename T>
    class Wrapper {
    public:T value;Wrapper(T v) : value(v) {} // 普通构造函数
    };// 自定义推导指引
    template<typename T>
    Wrapper(T) -> Wrapper<T>;
    
  • 注意:引入实参推导并不意味着降低了类型限制

    即使类模板的参数可以从构造函数参数中推导出来,这并不意味着模板参数可以是任何类型。模板参数仍然需要满足类模板中定义的任何类型约束或要求。

  • C++ 17 之前的解决方案:引入辅助模板函数

    template<typename T>
    class Wrapper {
    public:T value;Wrapper(T v) : value(v) {} // 普通构造函数
    };// 辅助模板函数
    template<typename T>
    Wrapper<T> make_Wrapper(T v) {return Wrapper<T>(v);
    }int main() {auto w = make_Wrapper(42); // 使用辅助函数创建 Wrapper 实例
    }
    

    C++中有很多类似的函数,如:make_pair

  • 类模板实参推导的限制

    • 类模板实参推导只适用于构造函数。
    • 如果构造函数重载,编译器需要能够从上下文推导出应该使用哪一个构造函数。
    • 如果存在多个可能的模板实例化,编译器将尝试找到最佳匹配。

三、Concepts(C++20)

详细内容可参考:https://en.cppreference.com/w/cpp/language/constraints

  C++模板的问题:没有对模板参数引入相应的限制,会造成如下两个问题:

  • 参数是否可以正常工作,通常需要阅读代码进行理解

  • 编译报错友好性较差(vector<int&>)

    C++编译器在模板实例化失败时生成的错误信息通常很长且难以理解。这是因为模板在编译时展开,如果模板参数不符合要求,编译器需要报告所有相关的错误。例如, vector<int&> 是一个错误用法,因为 vector 需要其元素类型是可复制的,而引用类型 int& 不满足这一要求。这将导致编译错误,但错误信息可能不会直接指出问题所在。

  C++20 引入了 Concepts(概念),这是一种新的类型系统特性,用于在编译期对模板参数进行更严格的约束。Concepts 允许开发者定义模板参数必须满足的条件,这些条件被称为编译期谓词,它们返回 truefalse 来表明模板参数是否符合预期

基本概念

  • concept:编译期谓词,它定义了一组类型必须满足的要求

    #include <iostream>
    #include <type_traits>template <typename T>
    concept IsAvail = std::is_same_v<T, int> || std::is_same_v<T, float>;int main()
    {return IsAvail<int>;
    }
    
  • constraints(约束):concept与 constraints ( require 从句)一起使用限制模板参数。通常置于表示模板形参的尖括号后面进行限制。

    #include <iostream>
    #include <type_traits>template <typename T>
    concept IsAvail = std::is_same_v<T, int> || std::is_same_v<T, float>;template <typename T>requires IsAvail<T>
    void fun(T input)
    {}int main()
    {fun(3);
    }
    

concept 的定义与使用

  • 包含一个模板参数的 concept

    • 使用 requires 从句

      #include <iostream>
      #include <type_traits>template <typename T>
      concept IsAvail = std::is_same_v<T, int> || std::is_same_v<T, float>;template <typename T>requires IsAvail<T>
      void fun(T input)
      {}int main()
      {fun(3);
      }
      
    • 直接替换 typename

      #include <iostream>
      #include <type_traits>template <typename T>
      concept IsAvail = std::is_same_v<T, int> || std::is_same_v<T, float>;template <IsAvail T>
      void fun(T input)
      {}int main()
      {fun(3);
      }
      
  • 包含多个模板参数的 concept

    #include <iostream>
    #include <type_traits>template <typename T, typename T2>
    concept IsAvail = !std::is_same_v<T, T2>;template <typename T, typename T2>requires IsAvail<T, T2>
    void fun(T input, T2 input2)
    {}int main()
    {fun(3, 3.14);
    }
    

    用做类型 constraint 时,少传递一个参数,推导出的类型将作为首个参数

    #include <iostream>
    #include <type_traits>template <typename T, typename T2>
    concept IsAvail = !std::is_same_v<T, T2>;template <IsAvail<int> T>
    void fun(T input)
    {}int main()
    {fun(3.14);
    }
    

requires表达式

注意区分requires表达式与requires从句的含义

  • requires 从句用于模板定义中,它指定了模板参数必须满足的条件。
  • requires 表达式是 requires 从句的一部分,它用于定义概念(Concepts)。
  • 简单表达式:表明可以接收的操作
  • 类型表达式:表明是一个有效的类型
  • 复合表达式:表明操作的有效性,以及操作返回类型的特性
  • 嵌套表达式:包含其它的限定表达式

requires 从句会影响重载解析与特化版本的选取

  • 只有 requires 从句有效而且返回为 true 时相应的模板才会被考虑

    当编译器进行函数调用时,它会尝试找到匹配的函数重载版本。如果一个模板的 requires 从句中的条件不满足,那么即使模板的其他部分与调用匹配,这个模板版本也不会被考虑。这意味着 requires 从句充当了一种编译期的筛选器,确保只有当条件满足时,相应的模板实例才会被考虑。

    #include <iostream>
    #include <type_traits>template <typename T>requires std::is_same_v<T, float>
    void fun(T input)
    {std::cout << "float";
    }template <typename T>requires std::is_same_v<T, int>
    void fun(T input)
    {std::cout << "int";
    }int main()
    {fun(3);		//第二个模板将会被调用
    }
    
  • requires 从句所引入的限定具有偏序特性,系统会选择限制最严格的版本

    当存在多个模板特化版本时,编译器会根据 requires 从句所引入的限定来选择最合适的特化。这些限定具有偏序特性

    #include <iostream>
    #include <type_traits>template <typename T>
    concept C1 = std::is_same_v<T, int>;template <typename T>
    concept C2 = std::is_same_v<T, float> || std::is_same_v<T, int>;template <C1 T>
    void fun(T input)
    {std::cout << "1";
    }template <C2 T>
    void fun(T input)
    {std::cout << "2";
    }int main()
    {fun(3);
    }
    

特化小技巧:在声明中引入“ A||B” 进行限制,之后分别针对 A 与 B 引入特化

#include <iostream>
#include <type_traits>template <typename T>requires std::is_same_v<T, int> || std::is_same_v<T, float>
class B;template <>
class B<int> {};template <>
class B<float> {};int main()
{B<double> x;
}

四、模板相关内容

1.数值模板参数与模板模板参数

数值模板参数

  模板可以接收(编译期常量)数值作为模板参数

  • 使用int类型的编译器常量

    其写法为:

    template <int a> 
    class Str;
    

    示例:

    template <int a>
    int fun(int x)
    {return x + a;
    }int main()
    {fun<3>(5);
    }
    
  • 使用类型与编译器常量的组合

    这种方式允许你指定一个类型 T 和一个该类型的编译期常量 value

    其语法为:

    template <typename T, T value> 
    class Str;
    

    示例:

    template <typename T, T a>
    int fun(int x)
    {return x + a;
    }int main()
    {fun<int, 3>(5);
    }
    
  • 使用 auto 关键字来简化模板参数的定义(C++17)

    在C++17中,可以使用 auto 关键字来简化模板参数的定义,使得模板参数可以自动推断为传递给它的值的类型。

    其语法为:

    template <auto value>
    class Str {// ...
    };
    

    示例:

    template <auto a>
    int fun(int x)
    {return x + a;
    }int main()
    {fun<3>(5);fun<true>(5);
    }
    
  • 接收字面值类对象与浮点数作为模板参数(C++20)

    C++20进一步扩展了模板非类型参数,允许使用字面值类对象和浮点数作为模板参数。

    其语法为:

    template <double value>
    class FloatStr {// ...
    };
    

    支持还不完整,有些编译器不支持

模板模板参数

  在 C++ 中,模板可以接收另一个模板作为参数

  • 模板的模板参数(C++17之前)

    即一个模板的模板参数为模板T。在 C++17 之前,模板的模板参数需要显式指定类型说明符 class

    其语法为:

    template <template<typename T> class C>
    class Str {// ...
    };
    
  • C++17开始允许省略类型说明符

    C++17 标准放宽了对模板的模板参数的语法要求,允许在模板的模板参数中省略类型说明符 class

    其语法为:

    template <template<typename T> typename C>
    class Str {// ...
    };
    

    示例:

    #include <vector>template <template<typename T> typename C>
    void fun() {C<int> tmp;
    };int main()
    {fun<std::vector>();
    }
    
  • C++17 开始,模板的模板实参考虑缺省模板实参

    如:上面的vector类模板实际有两个参数,第二个参数为缺省实参

    支持还不完整,有些编译器不支持

2.别名模板与变长模板

别名模板

  在 C++ 中,using 关键字可以用于引入别名,可以使用 using 引入别名模板。

  • 为模板本身引入别名

    template <typename T>
    class MyClass {// ...
    };// 为模板本身引入别名
    template <typename T>
    using MyAlias = MyClass<T> ;
    int main()
    {// 使用别名创建对象MyAlias<int> myObject;
    }
    
  • 为类模板的成员引入别名

    template <typename T>
    class MyClass {
    public:template <typename U>class InnerClass {// ...};using InnerType = InnerClass<T>; // 为 InnerClass 模板的特定实例引入别名
    };int main()
    {// 使用别名访问类模板的成员MyClass<int>::InnerType myInnerObject;
    }
    
  • 别名模板不支持特化,但可以为基于类模板的特化引入别名,以实现类似特化的功能

    template <typename T>
    class MyClass {// ...
    };// 特化模板
    template <>
    class MyClass<double> {// ...
    };int main()
    {// 为特化的模板引入别名using MyDoubleClass = MyClass<double>;
    }
    

变长模板

详细内容可参考:https://zh.cppreference.com/w/cpp/language/parameter_pack

  C++中的变长模板(Variadic Templates),也称为参数包(Parameter Packs),是一种强大的模板特性,允许模板接受任意数量的模板参数。

  • 变长模板参数与参数包

    变长模板参数使用省略号(...)来表示,可以与模板参数列表中的其他参数一起使用

    template <typename... Types>
    class Tuple {// Types 是一个类型参数包,可以包含任意数量的类型
    };
    
  • 变长模板参数可以是数值、类型或模板

    • 类型参数包:可以用于模板的类型参数。

      #include <iostream>template <typename ... T>
      void fun(T... args)
      {}int main()
      {fun<int, double, char>(3, 5.3, 'c');
      }
      

      变长函数模板可以用任意数量的函数实参调用

    • 数值参数包:可以用于模板的非类型参数。

      template <int... Values>
      void printInts() {// 使用递归或迭代来打印 Values 中的整数
      };
      
    • 模板参数包:可以用于模板的模板参数。

      template <template <typename> class... Templates>
      class TemplateHolder {// Templates 是一个模板参数包
      };
      
  • sizeof... 操作(C++11)

    sizeof... 操作符用于获取参数包中的参数数量

    template<class... Types>
    struct count
    {static const std::size_t value = sizeof...(Types);
    };
    
  • 注意变长模板参数的位置

    • 在主类模板中,模板形参包必须是模板形参列表的最后一个形参。特化模板没有这个限制

    • 在函数模板中,模板参数包可以在列表中更早出现,只要其后的所有形参都可以从函数实参推导或拥有默认实参即可:

    template<typename U, typename... Ts>    // OK:能推导出 U
    struct valid;
    // template<typename... Ts, typename U> // 错误:Ts... 不在结尾
    // struct Invalid;template<typename... Ts, typename U, typename=void>
    void valid(U, Ts...);    // OK:能推导出 U
    // void valid(Ts..., U); // 不能使用:Ts... 在此位置是不推导语境valid(1.0, 1, 2, 3);     // OK:推导出 U 是 double,Ts 是 {int, int, int}
    

3.包展开与折叠表达式

包展开(C++11):

  通过包展开技术操作变长模板参数。

  • 模式T后随省略号且其中至少有一个形参包的名字会被展开成零个或更多个逗号分隔的模式实例,其中形参包的名字按顺序被替换成包中的各个元素。

    示例:

    template<class... Us>
    void f(Us... pargs) {}template<class... Ts>
    void g(Ts... args)
    {f(&args...); // “&args...” 是包展开// “&args” 是它的模式
    }
    int main()
    {g(1, 0.2, "a"); // Ts... args 会展开成 int E1, double E2, const char* E3// &args... 会展开成 &E1, &E2, &E3// Us... 会展开成 int* E1, double* E2, const char** E3   
    }
    

    编译器会翻译成

    template<class ... Us>
    void f(Us... pargs)
    {
    }/* First instantiated from: insights.cpp:7 */
    #ifdef INSIGHTS_USE_TEMPLATE
    template<>
    void f<int *, double *, const char **>(int * __pargs0, double * __pargs1, const char ** __pargs2)
    {
    }
    #endiftemplate<class ... Ts>
    void g(Ts... args)
    {f(&args... );
    }/* First instantiated from: insights.cpp:12 */
    #ifdef INSIGHTS_USE_TEMPLATE
    template<>
    void g<int, double, const char *>(int __args0, double __args1, const char * __args2)
    {f(&__args0, &__args1, &__args2);
    }
    #endifint main()
    {g(1, 0.20000000000000001, "a");return 0;
    }
    
  • 如果两个形参包在同一模式中出现,那么它们同时展开而且长度必须相同:

    template<typename...>
    struct Tuple {};template<typename T1, typename T2>
    struct Pair {};template<class... Args1>
    struct zip
    {template<class... Args2>struct with{typedef Tuple<Pair<Args1, Args2>...> type;// Pair<Args1, Args2>... 是包展开// Pair<Args1, Args2> 是模式};
    };int main()
    {typedef zip<short, int>::with<unsigned short, unsigned>::type T1;// Pair<Args1, Args2>... 会展开成// Pair<short, unsigned short>, Pair<int, unsigned int> // T1 是 Tuple<Pair<short, unsigned short>, Pair<int, unsigned>>// typedef zip<short>::with<unsigned short, unsigned>::type T2;// 错误:包展开中的形参包包含不同长度   
    }
    

使用包展开技术操作变长模板参数的基本应用:

#include <iostream>void fun()
{}template <typename U, typename... T>
void fun(U u, T... args)
{std::cout << u << std::endl;fun(args...);
}int main()
{fun(1, 2, "hello", "world");
}

运行结果:

1
2
hello
world

折叠表达式(C++17):简化变长模板参数操作

详细内容可参考:https://zh.cppreference.com/w/cpp/language/fold

  • 基于逗号的折叠表达式应用

    示例:对上述代码进行改写

    #include <iostream>void fun()
    {}template <typename... T>
    void fun(T... args)
    {((std::cout << args << std::endl), ...);
    }int main()
    {fun(1, 2, "hello", "world");
    }
    
  • 折叠表达式用于表达式求值,无法处理输入(输出)是类型与模板的情形

4.完美转发与lambda表达式模板

完美转发(C++11): std::forward 函数

  完美转发允许模板函数在转发参数时保留参数的值类别(左值或右值)。这是通过 std::forward 函数和万能引用(也称为转发引用)实现的。

  • 万能引用

    万能引用是一个使用双&&声明的引用类型。它可以接受左值、右值,或者通过模板参数推导为 T&T&&

    template <typename T>
    void func(T&& arg) {// arg 是一个万能引用,可以绑定到左值或右值
    }
    
  • std::forward 函数

    std::forward 是一个模板函数,用于实现完美转发。它的作用是:

    • 当模板参数 UT 相同的时候,std::forward<T>(arg)arg 视为 T 类型的左值引用或右值引用,这取决于 arg 在声明时的类型。
    • UT 不同的时候,std::forward<T>(arg)arg 视为 T 类型的右值引用。
    template <typename T>
    void func(T&& arg) {// 使用 std::forward 实现完美转发someOtherFunction(std::forward<T>(arg));
    }
    
  • 完美转发的使用场景

    完美转发通常用于模板函数或模板类中,特别是那些需要转发其参数给其他函数或构造函数的模板。这样可以保证:

    • 如果原始参数是一个左值,它在转发后仍然是左值。
    • 如果原始参数是一个右值,它在转发后仍然是右值。

示例:

#include <iostream>void process(int& i) {std::cout << "process(int&)" << std::endl;
}void process(int&& i) {std::cout << "process(int&&)" << std::endl;
}template <typename T>
void wrapper(T&& arg) {process(std::forward<T>(arg));
}int main()
{int x = 3;wrapper(x);wrapper(3);
}

编译器翻译结果:

image-20240607213648027

运行结果:

process(int&)
process(int&&)

在这个示例中,wrapper 函数接受一个万能引用参数。根据传入 wrapper 的参数是左值还是右值,std::forward 将正确地将参数转发给 process 函数,保持其值类别。

lambda表达式模板(C++20):

详细内容可参考:https://zh.cppreference.com/w/cpp/language/lambda

5.消除歧义与变量模板

使用 typename 与 template 消除歧义

  • 使用 typename 表示一个依赖名称是类型而非静态数据成员

    当你在类型上下文中使用依赖名称,并且该名称表示一个类型时,你可以使用 typename 来消除歧义。这通常发生在通过模板参数访问嵌套类型时。

    template <typename T>
    class Outer {
    public:template <typename U>class Inner {};// 使用 typename 来消除歧义,表示 Inner 是一个类型typedef typename Outer<T>::Inner<int> TypedInner;
    };
    

    在这个例子中,Outer<T>::Inner<int> 是一个依赖名称,它依赖于模板参数 T。使用 typename 告诉编译器 Inner<int> 是一个类型

  • 使用 template 表示一个依赖名称是模板

    当你需要指定一个依赖名称是模板时,可以使用 template 关键字。

    template <typename T>
    class MyClass {
    public:template <typename U>void function() {// 使用 template 来消除歧义,表示 function 是一个模板MyClass<T>::template function<U>();}
    };
    

    在这个例子中,MyClass<T>::function<U>() 是一个依赖名称,它表示一个模板。使用 template 告诉编译器 function 是一个模板,而不是一个静态成员或类型。

  • template 与成员函数模板调用

    成员函数模板是类模板内部定义的模板。当你在类模板外部实例化一个成员函数模板时,你需要使用 template 来指定模板实例化。

    template <typename T>
    class MyClass {
    public:template <typename U>void memberFunction(U param) {// ...}
    };int main() {MyClass<int> myObject;// 调用成员函数模板,需要使用 template 来指定模板参数myObject.template memberFunction<double>(3.14);
    }
    

    编译器翻译成

    template<typename T>
    class MyClass
    {public: template<typename U>inline void memberFunction(U param){}
    };/* First instantiated from: insights.cpp:11 */
    #ifdef INSIGHTS_USE_TEMPLATE
    template<>
    class MyClass<int>
    {public: template<typename U>inline void memberFunction(U param);/* First instantiated from: insights.cpp:13 */#ifdef INSIGHTS_USE_TEMPLATEtemplate<>inline void memberFunction<double>(double param){}#endif// inline constexpr MyClass() noexcept = default;
    };#endifint main()
    {MyClass<int> myObject;myObject.memberFunction<double>(3.1400000000000001);return 0;
    }
    

    在这个例子中,memberFunctionMyClass 的一个成员函数模板。在 main 函数中,我们使用 template 关键字来指定 memberFunction 的模板参数 double

变量模板(C++14):

  C++14 引入了变量模板,这是一种新的模板类型,允许模板定义变量。

  • 基本形式的变量模板

    template <typename T>
    T pi = T(3.1415926);
    

    pi 是一个变量模板,它对于每个类型 T 都有一个与之对应的实例。注意,这里使用类型转换 T(3.1415926) 来确保值 3.1415926 根据模板参数 T 的类型进行适当的转换。

  • 使用变量模板

    只需要指定所需的类型

    double piDouble = pi<double>; // 使用 double 类型的 pi
    int piInt = pi<int>;          // 使用 int 类型的 pi
    
  • 其他形式的变量模板

    • 编译时常量

    • 类型属性

    • 内联变量模板

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

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

相关文章

【mysql】数据报错: incorrect datetime value ‘0000-00-00 00:00:00‘ for column

一、问题原因 时间字段在导入值0000-00-00 00:00:00或者添加 NOT NULL的时间字段时&#xff0c;会往mysql添加0值&#xff0c;此时可能出现此报错。 这是因为当前的MySQL不支持datetime为0&#xff0c;在MySQL5.7版本以上&#xff0c;默认设置sql_mode模式包含NO_ZERO_DATE, N…

Python爬取城市空气质量数据

Python爬取城市空气质量数据 一、思路分析1、寻找数据接口2、发送请求3、解析数据4、保存数据二、完整代码一、思路分析 目标数据所在的网站是天气后报网站,网址为:www.tianqihoubao.com,需要采集武汉市近十年每天的空气质量数据。先看一下爬取后的数据情况: 1、寻找数据…

大模型基础——从零实现一个Transformer(1)

一、Transformer模型架构图 主要模块&#xff1a; embedding层&#xff1a; Input/Output Embedding&#xff1a; 将每个标记(token)转换为对应的向量表示。 Positional Encoding&#xff1a;由于没有时序信息&#xff0c;需要额外加入位置编码。 N个 block堆叠: Multi-Head …

【QT5】<总览四> QT常见绘图、图表及动画

文章目录 前言 一、QFile类读写文件 二、QPainter绘简单图形 三、QChart图表 四、QPropertyAnimation属性动画 五、Q_PROPERTY宏简介 六、自定义属性动画 前言 承接【QT5】&#xff1c;总览三&#xff1e; QT常用控件。若存在版权问题&#xff0c;请联系作者删除&#…

UE5 Mod Support 思路——纯蓝图

原创作者&#xff1a;Chatouille 核心功能 “Get Blueprint Assets”节点&#xff0c;用于加载未来的mod。用基础类BP_Base扩展即可。打包成补丁&#xff0c;放到Content\Paks目录下&#xff0c;即可让游戏访问到内容。 与文中所写不同的地方 5.1或者5.2开始&#xff0c;打…

uniapp封装picker选择器组件,支持关键字查询

CommonPicker.vue组件 路径在 components\CommonPicker.vue <template><view><uni-easyinput v-model"searchQuery" :placeholder"placeholder" /><picker :range"filteredOptions" :range-key"text" v-model&…

从零开始:疾控中心实验室装修攻略,让你的实验室一步到位!

在当今充满挑战和变化的世界中&#xff0c;疾病的控制和预防成为了人类生存与发展的重要课题。而疾控中心作为防控疾病的核心机构&#xff0c;其疾控中心实验室设计建设显得尤为重要。下面广州实验室装修公司小编将分享疾控中心实验室设计建设方案&#xff0c;为疾病防控工作提…

玩转STM32-通信协议SPI(详细-慢工出细活)

文章目录 一、SPI的基础知识1.1 接口定义1.2 单机和多机通信 二、STM32的SPI工作过程2.1 从选择&#xff08;NSS&#xff09;脚管理2.2 时钟相位与极性2.3 SPI主模式2.4 SPI从模式 三、应用实例 一、SPI的基础知识 1.1 接口定义 SPI系统可直接与各个厂家生产的多种标准外围器…

ChatGPT-4o独家揭秘:全国一卷高考语文作文如何轻松斩获满分?

​一、2024年全国一卷高考 二、2018年全国一卷高考 三、2016年全国一卷高考 一、2024年全国一卷高考 技术进步的悖论&#xff1a;我们的问题真的在减少吗&#xff1f; 引言 随着互联网的普及和人工智能的应用&#xff0c;越来越多的问题能够快速得到解答。然而&#xff0c;这引…

网络空间安全数学基础·同余式

6.1 剩余系&#xff08;掌握&#xff09; 6.2 同余式概念与一次同余式&#xff08;熟练&#xff09; 6.3 中国剩余定理&#xff08;熟练&#xff09; 6.1 剩余系 设m是正整数&#xff0c;模m同余的全体整数是一个模m剩余类&#xff0c;即可表示为a qmr&#xff0c; 0≤r<…

牛客练习赛126(O(n)求取任意大小区间最值)

牛客练习赛126(O(n)求取任意大小区间最值) 牛客练习赛126 A.雾粉与签到题 题意&#xff1a;给出长度为n的数组, 顺序选出任意三个元素&#xff0c;最小化第二个元素 思路&#xff1a; 遍历除了第一个和最后一个元素取最小值即可 AC code&#xff1a; void solve() {int…

使用 tc (Traffic Control)控制网络延时

设置网络延时 1500ms 800ms tc qdisc add dev eth0 root netem delay 1500ms 800msping 测试 ping www.baidu.com取消设置网络延时 sudo tc qdisc del dev eth0 root

inflight 守恒和带宽资源守恒的有效性

接着昨天的问题&#xff0c;inflight 守恒的模型一定存在稳定点吗&#xff1f;并不是。如果相互抑制强度大于自我抑制强度&#xff0c;系统也会跑飞&#xff1a; 模拟结果如下&#xff1a; 所以一定要记得 a < b。 比对前两个图和后两个图的 a&#xff0c;b 参数关系&am…

PS初级|写在纸上的字怎么抠成透明背景?

前言 上一次咱们讲了很多很多很多的抠图教程&#xff0c;这次继续。。。最近有小伙伴问我&#xff1a;如果是写在纸上的字&#xff0c;要怎么把它抠成透明背景。 这个其实很简单&#xff0c;直接来说就是选择通道来抠。但有一点要注意的是&#xff0c;写在纸上的字&#xff0…

鸿蒙开发的南向开发和北向开发

鸿蒙开发主要分为设备开发和应用开发两个方向&#xff0c;也叫南向开发和北向开发&#xff1a; 鸿蒙设备开发(南向开发&#xff09;&#xff0c;要侧重于硬件层面的开发&#xff0c;涉及硬件接口控制、设备驱动开发、鸿蒙系统内核开发等&#xff0c;目的是使硬件设备能够兼容并…

android antirollback verno 获取方法

ReadRollbackIndex.exe 获取 调查avbVBMeta结构体 typedef struct AvbVBMetaImageHeader { /* 0: Four bytes equal to "AVB0" (AVB_MAGIC). */ uint8_t magic[AVB_MAGIC_LEN]; /* 4: The major version of libavb required for this header. */ uint32_t…

美团面试:百亿级分片,如何设计基因算法?

尼恩说在前面 在40岁老架构师 尼恩的读者交流群(50)中&#xff0c;最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格&#xff0c;遇到很多很重要的架构类/设计类的场景题&#xff1a; 1.说说分库分表的基因算法&#xff1f…

react native中内容占位效果

react native中内容占位效果 效果实例图实例代码skeleton.jsx 效果实例图 实例代码skeleton.jsx import React, { useEffect, useRef } from "react"; import { Animated, StyleSheet, View } from "react-native"; import { pxToPd } from "../../.…

寻找python库的安装路径

以pip库为例 要找到并修改 pip 库中的 __pip-runner__.py 文件&#xff0c;您可以按照以下步骤操作&#xff1a; 找到 pip 库的安装路径&#xff1a; 通常&#xff0c;Python 库会安装在您的虚拟环境或全局 Python 包目录中。您可以通过以下命令来找到 pip 库的路径&#xff1…

代码随想录算法训练营day31|455.分发饼干、376.摆动序列、53.最大子序和

分发饼干 455. 分发饼干 - 力扣&#xff08;LeetCode&#xff09; 贪心算法&#xff0c;让每个饼干给到能够满足的孩子&#xff0c;所以需要对饼干尺寸和孩子的满足值先进行排序&#xff0c;然后针对每一个饼干的尺寸&#xff0c;挑选恰好能够满足的孩子&#xff08;这里表述…