模板笔记007 - 模板的声明、形参、实参

本文详细介绍了C++中的模板,包括模板的声明、模板参数(形参)、模板实参。讨论了类模板和函数模板的基本概念,以及模板参数的类型参数、非类型参数和模板的模板参数。还提到了模板实参的显式指定、实参推导以及模板实参的限制。此外,文章还涵盖了模板的友元函数和友元模板的相关知识。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

模板笔记007 - 声明 形参 实参

声明

  • 类模板和函数模板是c++的两种基本类型的模板
    模板的声明需要引入一个参数化子句:template <...>

  • 联合模板也是类模板的一种

  • 和普通的函数一样,模板函数也可以有缺省参数

template <typename T>
void func1(Stack<T> const&, int max =10)
{}

template <typename T>
void func2(Array<T>*, T const& = T的默认构造 ) // 只是为了语法高亮,后一个参数是 T const& = T()
{}

对于基本类型,T()为0

  • 参数化除了用在两种基本类型的模板之外,还可以用于一下三种声明:

    1. 类模板成员函数的定义
    2. 类模板嵌套类成员的定义
    3. 类模板的静态数据成员的定义

    需要注意的是上面3种特殊的情况都不是模板

    template <int I>
    class A
    {
        void open();
        class B;
        static int num;
    };

    template <int I>
    void A<I>::open()
    {}

    template <int I>
    class A<I>::B
    {};

    template <int I>
    int A<I>::num = 0;

上面这3种情况,都不是使用自身的模板,而是使用外围类模板,参数也是由外围类模板决定
书上并不建议我们称她们为模板

  • 虚函数

    成员函数模板不能称为虚函数,相反,类模板的普通成员函数可以成为虚函数

  • 每个模板都有一个名字,且在所属作用域下是唯一的
    int a;
    class a; // ok

    template <typename T>
    class a; // error
  • 模板名字具有链接
    extern "C++" template <typename T>
    void func1() {} // 标准链接,是默认的,可以忽略不写,一般也没有写

    extern "C" template <typename T>
    void func2() {} // error, 不能有C链接

    extern "python" template <typename T>
    void func3() {} // ok, 非标链接,属于兼容性链接,目前(2002年)还没有编译器实现,

模板通常具有外部链接(唯一的例外是static函数模板),所以函数内部不能声明模板(因为函数内部都是局部对象,不能保证模板名字的唯一性)

模板的声明只能出现在namespace scope或是class scope里

  • 模板的基本类型有两种:类模板和函数模板。那什么叫基本模板?

    即普通声明(没有在模板名称后面加< >)

    template <typename T> class A;
    template <typename T> class B<T>;
    template <typename T> void func1(T*);
    template <typename T> void func2<T>(T*);
    // 13就是基本模板,24就不是.偏特化和全特化就是非基本模板,函数模板大部分是基本模板

模板参数 (形参)

  • 模板声明时需要引入参数化子句,模板参数就在参数化子句里 template <typename T, int I>

    模板参数分3种:类型参数;非类型参数;模板的模板参数

    参数名如果在接下来没有用到,可以省去不写,这点和函数一样

    在同一对< >中,后面的参数可以引用前面的参数,但前面的不能引用后面的。

    template <typename T, T* root, template<T*> class B>
    class A;
  • 类型参数可以通过template或是class引入

    在模板内部,类型参数的作用相当于typedef 类型定义

    非类型参数是在编译期或是链接期可以确定的值,可以是以下类型:

    1. 整形 枚举
    2. 指针类型(普通对象指针,函数指针,指向成员的指针类型)
    3. 引用类型(对象引用,函数引用)

    除此之外,其他类型都不能作为非类型参数使用

        template <typename T, typename T::B* BB>
        class A;

    这是一种特殊的情况,用typename来声明非类型参数, 此处typename 是用来修饰T::的

    1. 函数和数组都能用来做模板的非类型参数,只是需要先将他们转换成指针类型
    2. 非类型参数不能具有static / mutable 修饰,可以有const / volatile , 如果这两个用于修饰非类型参数整个,那么编译器将忽略修饰
    3. 非类型模板参数只能是右值,不能被取址, 不能被赋值
  • 模板的模板参数

    1. 模板的模板参数表示的是一个占位符 placehoder
    2. 只能用class 不能用struct 或是 union

      template <template<typename X> class C> // ok
      void f(C<int>* p);
      
      template <template<typename X> struct C> // error
      void f(C<int>* p);
      
      template <template<typename X> union C> // error
      void f(C<int>* p);
    3. 上面的C就是模板的模板参数,X就是模板的模板参数的参数,X可以有缺省值

      template <template<typename T, typename A = MyA> class B>
      class C
      {
          B<int> b;
          B<int, MyA> c; // b 和 c 是等同的,
      };
    4. 模板的模板参数的模板参数名只能被模板的模板参数的其他参数声明使用

    这个比较绕口,英文如下:the name of a template parameter of a template template parameter can be used only in the declaration of other parameters of that template template parameter.
    翻译一下:template template parameter 是上面的B, a template parameter of B 指B的模板参数T和A,整句话连在一起就是:B的模板参数只能被B的其他模板参数使用.

    通常模板的模板参数的参数名称在后面并不会用到, 所以名称也常常省略掉

  • 模板参数的缺省值

    缺省值的规则和函数缺省值的规则一样,需要注意一点的是:缺省值不能依赖自身的参数,但可以依赖前面的参数;再者缺省值不能重复声明:

    template <typename T = void>
    class A;
    
    template <typename T = void>
    class A; // error:重复出现缺省实参

模板实参

  • 什么是模板实参?

    模板实例化时,用来替换模板参数的这个值就是实参,上面提到过的缺省值,也是实参(这点和函数类似)

  • 有哪些方式来指定实参
    1. 显式模板实参:模板名称后面跟< int,double>, 组合起来称为template-id
    2. 注入式类名称,注入式表示在模板类的作用域里面使用了模板名,这时模板名表示的是template-id 等同于X< P1,P2… >
    3. 缺省模板实参,上面已经介绍过
    4. 实参推导,也叫实参演绎:对于非显示指定的函数模板实参,根据上下文会自动推导。如果所有的模板实参都可以通过推导获得,那么在函数模板名称后面就不需要指定< >
  • 函数模板的实参
    max<int>(1, 3); //  显式指定模板实参
    max(1.0, 2.0); // 自动推导

对于得不到自动推导机会的参数,我们把她们放在模板参数列表的开始处,用于显式指定

  • SFINAE

substitution failure is not an error 替换失败并不是错误,她的原则是允许创建无效的类型,但不允许试图计算无效的表达式

    template <int I> void f(int (&)[24/(4-I)]); // error:不能应用sfinae

对应不同类别的模板参数,介绍一下相应的实参

上面也说到了模板参数有3类:类型参数;非类型参数;模板的模板参数

  • 类型实参
    平常大部分类型都可以用作类型实参,除了下面两种特殊情况
    1. 局部类和局部枚举不能作为模板的类型实参,换句话:函数定义内部声明的类型不行
    2. 未命名的类或者未命名的枚举,有个例外:使用typedef 声明的未命名类/枚举可以作为类型参数
    template <typename T> class List{};

    typedef struct{double x, y, z;} Point;
    typedef enum {red, green, bule} *ColorPtr;

    int main()
    {
        struct B
        {
            int a;
            int b;
        };

        List<B*> error1;       // 局部类不行
        List<ColorPtr> error2; // *ColorPtr 表示的是类型, ColorPtr不是
        List<Point> ok;        // 

        return 0;
    }

需要注意的是:替换之后,需要确保替换之后的构造是有效的

  • 非类型实参

    非类型实参可以是以下值:

    1. 另一个类型正确的非类型实参
    2. 编译期的整形常值,或是枚举。隐式转换成功即可
    3. 前面有&的外部变量或是函数名,对于数组和函数,前面的&可以省略。这类实参是用来匹配指针类型
    4. 对于引用类型的非类型形参,前面没有&的外部变量和外部函数也是ok的
    5. 一个指向成员的指针常量。eg:类似&C::m,其中C是类名,m是非静态成员。这类实参是用来匹配成员指针的

    需要注意的是:当实参匹配的是指针类型或是引用类型时,有些类型转换是无效的

    1. 用户定义的类型转换。单参构造函数;重载类型转换运算符;派生类到基类的转换;
    2. 隐式转换。隐式转换唯一生效的情况是对实参使用const/volatile 修饰时
      template <typename T, T nontype_param>
      class A;
      
      A<int, 123>* a1; // 整形
      
      int i;
      A<int*, &i>* a2; // 外部变量的地址
      
      void f();
      void f(int);
      A<void(*)(int), f>* a3; // 重载解析选择了f(int); f 前面的&会被省略
      
      class B
      {
      public:
          int a;
          static bool b;
      };
      A<bool&, B::b>* a4; // 引用类型,&可以省略,类的静态成员都可以这样用
      A<int B::*, &B::a>* a5; // 指向成员的指针,非静态成员
      
      template <typename T>
      void func();
      A<void(), &func<double>>* a6; // 函数模板实例就是函数

    有一些值不能作为非类型实参:空指针/浮点类型/字符串。针对字符串,我们可以用一个extern数组变量来存储,这是一种折中的办法。

    下面是一些错误例子:

    template <typename T, T nontype_params>
    class A;
    
    class base
    {
    public:
        int i;
    };
    
    class derived : public base
    {}obj;
    
    A<base*, &obj>* error1; // 这里的类型转换是无效的
    A<int&, base.i>* error2; // 域运算符. 后面的变量不会被当成变量
    
    int a[10];
    A<int*, &a[0]>* error3; // 单一数组元素的地址并不可取
  • 模板的模板实参

    模板的模板实参必须是一个类模板(class struct union),下面这句话要注意:

    在匹配过程中,模板的模板实参的缺省值不会被考虑,如果模板的模板参数具有缺省值,那么会匹配时会考虑
    翻译:

    
    #include <list>
    
    
    // list 的声明
    namespace std
    {
        template <typename T, typename Allocator = allocator<T>>
        class list;
    }
    
    template <typename T1,
              typename T2,
              template <typename> class Container>
    class AB
    {
        Container<T1> a;
        Container<T2> b;
    };
    
    int main()
    {
        AB<int, double, std::list> ab; // 会报错:std::list是具有多参数的模板
    
        ...
    }
    
    // 这里模板的实参用的是std::list ,她是具有缺省参数的,但这儿不考虑
    // 需要考虑的是类模板AB的模板参数,如果加上缺省值就ok了, 将第三个参数改成如下就行
    template <typename = std::allocator<T>> class Container 
  • 实参的等价
    1. 当每个对应的实参都相等时,称两个模板是等价的:
      A< int, 3+2>p1; A< int, 1*5> p2; 是等价的
    2. 函数模板实例化之后,和普通函数不等价。
    3. 类的成员函数模板不能修饰为虚函数,因为成员函数模板和普通函数不等价
    4. 类的构造函数模板实例化的构造函数一定不是缺省的拷贝构造函数

友元

声明友元是指:授予”某个类或函数访问友元声明所在的类”的权力

  • 如果把类模板的实例声明为其他类或者类模板的友元,该类模板一定要可见,对于普通类就没有这个要求,说白一点:如果类模板实力声明是友元,要么这个类模板要么有前置声明,要么可见
  • 友元函数
    template <typename T1, typename T2>
    void func(T1, T2);

    class A
    {
        // 友元函数模板实例就是友元函数, 写法就是在函数名func后面加<>
        // 模板实例化的参数有两种方式确定:<>里的特化,()里的自动推导
        // 这两种确定参数的方式可以混合用,只是两者需要一致

        friend void func<>(int, int); // 自动推导
        friend void func<int, int>(int, int); // 全特化和自动推导一致
        friend void func<char>(char, int);  // 偏特化和自动推导一致
        friend void func<char>(char&, char);  // 不一致
        friend void func<>(int, int){}  // 错误:友元声明不允许出现定义
    };

如上例,友元函数模板实例的写法如下:
1. friend 关键字
2. 函数返回类型
3. 函数名
4. <模板实参>
5. (参数列表)

4和5部分都只有一个作用,确定实例化的参数。其中4是特化,5是自动推导,也叫自动演绎

如果友元函数名后面没有< >, 那有可能是下面两种情况之一
1. 如果函数名的形式不是::func, 那就说明这个友元函数是一个普通的友元函数,而不是一个友元函数模板实例,如果是首次声明,可以是定义
2. 如果函数名前面有::func, 那么在友元之前,一定要先声明函数或函数模板,匹配的过程中优先匹配普通函数,受限的友元不能是定义

    // 术语
    // 受限:函数名的形式如 ::func
    // 非受限:函数名前面没有双冒号(域运算符)
    void func(void*);

    template <typename T>
    void func(T);

    class A
    {
        friend void func(int){} // 非受限,表示普通函数
                                // 未匹配,表示首次声明,可以定义
        friend void ::func(void*); // 受限,但会优先匹配到普通函数
        friend void ::func(int); // 受限,匹配到了函数模板实例
        friend void ::func<double*>(double*); // 受限,但带了<>就表示是函数模板实例,只有一个前提条件:函数模板要可见
        friend void ::func_other(){}; // 受限的友元不能是一个定义
    };
  • 类模板中的友元函数 友元模板 以后遇到了再分析
<think>好的,我需要解释C++模板形参实参的区别。首先,根据用户提供的引用内容,特别是引用[1]、[2]、[3],我需要理清模板形参实参的基本概念。 模板形参是在定义模板声明的参数,比如template<typename T>中的T,这属于形参。而实参是在实例化模板时具体传入的类型或值,比如vector<int>中的int。引用[1]提到,当函数参数是模板类型参数的引用时,编译器会根据实参推断形参类型,比如fl(i)会让T被推断为int,而fl(ci)则推断为const int。这说明形参的类型由实参决定,但引用绑定规则会影响推断结果。 接下来,引用[3]指出,当模板形参不是指针或引用时,实参的cv限定符引用会被忽略。例如,如果传递一个const int的引用给形参T,T会被推断为int而非const int。这展示了形参实参之间的类型转换规则,特别是在不同情况下的类型推导差异。 另外,引用[2]中的例子说明函数指针初始化时,模板实参推断会根据指针类型确定。例如,add函数模板被用于初始化int(*pf)(int,int),此时T被推断为int。这说明实参的类型在实例化模板时如何影响形参的具体类型。 还需要区分形参实参模板中的不同作用阶段:形参属于模板定义阶段,而实参在实例化时使用。引用[5]提到形参列表的声明方式,每个形参必须显式写出,即使类型相同。这强调了形参定义的语法要求,而实参则是在调用时传递的具体值或类型。 最后,结合引用[4]关于普通函数形参实参的关系,虽然模板中的情况更复杂,但基本概念类似,即形参是占位符,实参是实际传入的值或类型。在模板中,类型推导引用绑定规则会导致形参类型的调整,如const引用的处理方式不同。 总结起来,模板形参模板定义时的参数占位符,实参是实例化时提供的具体类型或值,两者通过类型推导引用绑定规则相互作用,导致不同的推断结果类型转换。</think>### C++模板形参实参的区别解析 #### 1. **基本定义** - **模板形参**:模板定义中声明的参数,属于**类型占位符**。例如: ```cpp template<typename T> // T 是模板形参 class MyContainer {}; ``` - **模板实参**:实例化模板时提供的**具体类型或值**。例如: ```cpp MyContainer<int> obj; // int 是模板实参 ``` #### 2. **类型推导规则** - **当形参是引用时**(如 `T&`): - 实参的 `const` 属性会被保留。例如: ```cpp template<typename T> void f1(T& param); const int a = 10; f1(a); // T 被推断为 const int,param类型是 const int&[^1] ``` - **当形参非指针/引用时**(如 `T`): - 实参的 `const` 引用属性会被忽略。例如: ```cpp template<typename T> void f2(T param); const int& b = 20; f2(b); // T 被推断为 int,param类型是 int[^3] ``` #### 3. **应用场景差异** - **形参的作用**:定义模板时约束逻辑的通用性。 - **实参的作用**:实例化时确定具体行为。例如: ```cpp int (*pf)(int, int) = add; // 根据函数指针类型,T被推断为int[^2] ``` #### 4. **语法要求** - 形参必须显式声明,即使类型相同: ```cpp template<typename T1, typename T2> // 必须分开声明 void func(T1 a, T2 b); ``` - 实参可以是类型、常量表达式或模板的其他实例化结果。 #### 5. **关键区别总结** | 特性 | 模板形参 | 模板实参 | |--------------------|-----------------------------|-----------------------------| | **定义阶段** | 模板声明时定义 | 模板实例化时提供 | | **类型推导影响** | 受引用/指针修饰符影响[^1] | 决定形参的具体类型 | | **语法形式** | `typename T` 或 `class T` | 具体类型(如 `int`)或值 |
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值