C++程序员应了解的那些事(68)非类型模板参数

本文详细介绍了C++中的非类型模板参数的概念、限制及其使用方法。包括非类型形参的定义、允许的数据类型、实例化过程中的注意事项等。并通过多个示例展示了非类型模板参数在实际编程中的应用。

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

目录

 

模板非类型参数导入:

什么是非类型形参?

非类型形参的局限:

非类型实参的局限:

剖析&注解:

非类型模板参数使用举例:非类型函数模板参数 非类型类模板参数

<1>非类型类模板参数

<2>非类型函数模板参数

<3>非类型模板参数的限制(可以是常整数 或者指向外部链接对象的指针(或引用))

非类型模板参数限制——不可以使用内部链接对象


模板非类型参数导入:

什么是非类型形参

模板除了定义类型参数,我们还可以在模板定义非类型参数

非类型模板参数,顾名思义,模板参数不限定于类型,普通值也可作为模板参数。在基于类型的模板中,模板实例化时所依赖的是某一类型的模板参数,
你定义了一些模板参数(template<typename T>)未加确定的代码,直到模板被实例化这些参数细节才真正被确定。而非类型模板参数,面对的未加
确定的参数细节是指(value),而非类型。当要使用基于值的模板时,你必须显式地指定这些值,模板方可被实例化。

什么是非类型形参顾名思义,就是表示一个固定类型的常量而不是一个类型。
※ 固定类型是有局限的,只有整形,指针和引用才能作为非类型形参
※ 而且绑定到该形参的
实参必须是常量表达式,即编译期就能确认结果。

        ①表达式参数有一些限制,可以为整型、枚举、引用或者指针。double m不合法,但是double * pm和double& rm合法。②模板代码不能修改表达式的值,也不能使用使用参数的地址。实例化模板时,用作表达式参数的值必须是常量表达式。

非类型形参的局限:

1.浮点数不可以作为非类型形参,包括float,double。具体原因可能是历史因素,也许未来C++会支持浮点数;
2.类不可以作为非类型形参;
3.字符串不可以作为非类型形参;
4.整形,可转化为整形的类型都可以作为形参,比如int,char,long,unsigned,bool,short(enum声明的内部数据可以作为实参传递给int,但是一般不能当形参);
5.指向对象或函数的指针与引用(左值引用)可以作为形参。

非类型实参的局限:

1.实参必须是编译时常量表达式,不能使用非const的局部变量,局部对象地址及动态对象;
2.非const的全局指针,全局对象/全局变量(下面可能有个特例)都不是常量表达式;

3.由于形参的已经做了限定,字符串,浮点型即使是常量表达式也不可以作为非类型实参 ;
4.非类型模板参数限制——不可以使用内部链接对象 ※
备注:常量表达式基本上是字面值以及const修饰的变量
*************************************************************************************************

<例1-整型>
const int r = 777;
template<int  r>
void R()
{
    cout << r << endl;
}
----------------------
int main()
{
    R<666>();
    R<r>();

    const  int r2 = 888;
    R<r2>();

    int  r3 = 999; //局部变量  
    //  R<r3>();   //错误:包含非静态存储持续时间的变量不能用作非类型参数(非const的局部变量)
}
<例2-指针>
char   x1[] = "saaaa";//全局变量 
char * x2   = "qweqeq";//全局变量 
char * const  x3 = "qweqeq";//全局变量 指针常量 
(warning: ISO C++ forbids converting a string constant to 'char*' [-Wwrite-strings] 修改为:const char *x2 = "qweqeq";)
template<typename T, char* x>
void X(T  t)
{
    cout << t << ", " << x << endl;
};
---------------------------------
int main()
{
    X<int, x1>(3);     // 这是那个例外
    // X<int, x2>(4);  // 错误: 应为编译时常量表达式 
    // X<int, x3>(5);  // 错误:非const的全局指针! & 涉及带有内部链接的对象的表达式不能用作非类型参数 

    char *x4 = "adsas";//局部变量,告警:ISO C++ forbids converting a string constant to 'char*'
    // X<int, x4>(6);//  错误: 包含非静态存储持续时间的变量不能用作非类型参数 
    // X<int, "sdfsd">(7);//错误:字符串,浮点型即使是常量表达式也不可以作为非类型实参
}
<例3-整型>
template<int a>
void  A(const char(&p)[a])
{
    std::cout << "size : " << a << "    " << p << std::endl; 
}
-----------------------------------------
int main()
{
    A("hello") ;
    A<6>("hello");
}
<例4-引用>
struct Y {};
Y y;
template<const Y& b>
struct Z {};

int q = 1;
template<const int& q> 
struct Q {};
--------------------
int main()
{
    Z<y> z;
    Q<q>  q1;
}
<例5-数组指针>
int b[5] = {11,22,33,44};
template<int(&pa)[5]> 
void B()
{
    cout << pa[1] << endl;
};
---------------------------
int main()
{
    B<b>(); // ok: no conversion
}
<例6-函数指针>
void f(int  a)
{
    cout << "a  is  "<<a << endl;
}
template<void(*pf)(int)>
void C()
{
    pf(111); 
};
----------------------------------
int main()
{
    C<&f>(); // ok: overload resolution selects f(int)
}
<例7-模板 模板参数>
template<typename T> class N { public:   int x; };   // primary template 通用模板
template<class T> class N<T*> { public:   long x; }; // partial specialization 偏特化

template<  template<typename> class V   > 
class  M
{
public:
    V<int>  y; // uses the primary template
    V<int*> z; // uses the partial specialization
};
------------------------------------
int main()
{
    M<N> m = M<N>(); //显示地调用内建类型的缺省构造函数
    cout << m.y.x << endl;
}
< 例8- 字符串&指针 >
template<char const* name>
class pointerT{
 
};

pointerT<"testVarChar">  p1;//错误

char a[]  = "saaa";;//全局变量
char a2[] = "saaa";;//局部变量,写在main函数里面
char *b   = "saaa";//全局变量
char *const c = "saaa";//全局变量,顶层指针,指针常量

pointerT<a>  p2;//正确
pointerT<a2> p22;//错误,局部变量不能用作非类型参数
pointerT<b>  p3;//错误,error C2975:“pointerT”的模板参数无效,应为编译时常量表达式
pointerT<c>  p4;//错误,error C2970: “c”: 涉及带有内部链接的对象的表达式不能用作非类型参数

剖析&注解:

①到底为什么字符串不能作为实参?

答:(1)我们看到上面p1的模板实参是"testVarChar",然而当我们在另一个编译单元(.cpp文件)同样声明这么一个模板实例时,这两个"testVarChar"的地址可能是不同的,编译器传递给模板时就会传递传递不同的地址,从而导致这两个模板实例是两个不同且不兼容的类型。这就是支持字符串的问题所在。

(2)两个内容完全相同的字符串字面常数可能存在两个不同的地址上。
 有个方法:  extern char const hello[] = "Hello World!"; 这里必须使用关键词 extern,因为 const array 采用内部链接(internal linkage)。

②.变量b和c作为模板实参为什么错误不同?

答:(1)首先解释b实参,b在这里看做是一个指针,是一个全局指针,但是他不是一个常量表达式,所以b不对; 我们再看看c,c相比于b对了一个const修饰符,表示这个指针是一个常量。然而const是一个比较特别的关键字,他具有内部链接属性(关于内连接参考博客 理解C++的链接:C++内链接与外链接的意义),也就是说仅在定义这个变量的文件内可见,不会造成不同编译单元的混编时的链接错误。

(2)这个特性对于模板来说可是有问题的,就像问题①所描述的,由于每个编译单元可能都有一个c变量,导致在编译时,实例化多个c,而且c的地址还不同,这就造成两个模板的实例是两个不同且不兼容的类型。

***************************************************************************************************************

非类型模板参数使用举例:非类型函数模板参数 非类型类模板参数

<1>非类型类模板参数

        这里我们使用一个新版本的Stack类模板,这类模板的底层容器是一个一维数组,数组的元素类型由模板类型参数typename T指定,而一位数组在初始化时必须指定其大小,这个大小便可通过一个非类型的模板参数int MAXSIZE指定。

template<typename T, int MAXSIZE>
class Stack
{
public:
    Stack():idx(0){}
    bool empty() const { return idx == 0;}
    bool full() const { return idx == MAXSIZE;}
    void push(const T&);
    void pop();
    T& top();
    const T& top() const;
private:
    int idx; 
    T elems[MAXSIZE];
}

template<typename T, int MAXSIZE>
void Stack<T, MAXSIZE>::push(const T& elem)  //定义类模板成员函数的形式
{
    if (full())
        throw std::out_of_range("Stack<>::push(): full stack");
    elems[idx++] = elem;
}

template<typename T, int MAXSIZE>
void Stack<T, MAXSIZE>::pop()
{
    if (!empty())
        idx--;
    else
        throw std::out_of_range("Stack<>::pop(): empty stack")
}
template<typename T, int MAXSIZE>
T& Stack<T, MAXSIZE>::top()
{
    if (empty())
        throw std::out_of_range("Stack<>::top(): empty stack");
    return elems[idx-1];
}
template<typename T, int MAXSIZE>
const T& Stack<T, MAXSIZE>::top() const
{
    if (empty())
        throw std::out_of_range("Stack<>::top(): empty stack");
    return elems[idx-1];
}

①注意上述代码中定义类模板成员函数的形式:

template<typename T, int MAXSIZE>
void Stack<T, MAXSIZE>::push(const T& elem)  //定义类模板成员函数的形式

客户端程序: 

try
{
    Stack<int, 10> int10Stack;
    Stack<int, 20> int20Stack;
    int20Stack.push(7);
    ...
}
catch(std::exception& ex)
{
    cout << ex.what() << endl;
    return EXIT_FAILURE;
}

②注:每个模板实例都有自己的类型,int10Stackint20Stack属于不同的类型,这两种类型之间也不存在显示或隐式的类型转换。

同样地,也可以为非类型模板参数指定缺省值:

template<typename T, int MAXSIZE = 20>
class Stack
{
    ...
}
这样在调用时:
Stack<int> intStack;  // == Stack<int, 20>

<2>非类型函数模板参数

同样地也可以为函数模板定义为非类型参数,如下的函数模板定义一组用于增加特定值的函数:

template<typename T, int VAL>
T addValue(const T& x)
{
    return x + VAL;
}

当然这样做纯粹为了演示,意义不大,我们可以将非类型参数的类型(这话有点绕)定义为类型参数:

template<typename T, T VAL>
T addValue(const T& x)
{
    return x+VAL;
}

借助标准模板库(STL),可以将这个函数模板的一个实例传递给集合中的每一个元素,将集合中的每一个值都增加一个预设的值:

std::transform(src.begin(), src.end(),      // 原容器(待转换)的起点和终点 
               dst.begin(),            // 目标容器的起点
               addValue<std::vector<int>::value_type, 10>);    // 操作或者函数(也可以是仿函数)

        另外还有一个问题需要注意,addValue<int, 5>是一个函数模板的实例化版本,而函数模板的实例化通常被视为用来命名一组重载函数的集合(即使该集合中只有一个元素)。在一些较老的C++标准里,重载函数的集合不能用于模板参数(如本例的transform())的演绎。于是,必须显式地将函数模板的实参强制转换为具体的类型:

std::transform(ivec.begin(), ivec.end(), dst.begin(),
                (int(*)(const int& ))addValue<int, 5>);

一个完整的演示程序如下:

int arr[] = {1, 2, 3, 4, 5};
vector<int> src(arr, arr+5), dst(5);
typedef vector<int>::value_type value_type;
transform(src.begin(), src.end(), dst.begin(), 
            (value_type (*)(const value_type&)addValue<value_type, 5>);
copy(dst.begin(), dst.end(), ostream_iterator<value_type>(cout, " "));// ostream_iterator 在<iterator>的std命名空间中

ostream_iterator效果示例:

#include<functional>  
#include<iterator> 
#include <iostream>
int main()
{
    int arr[] = {1, 2, 3, 4, 5};
    vector<int> src(arr, arr+5), dst(5);
    typedef vector<int>::value_type value_type;
    copy(src.begin(), src.end(), std::ostream_iterator<value_type>(cout, "*"));
}
输出:
1*2*3*4*5*

<3>非类型模板参数的限制(可以是常整数 或者指向外部链接对象的指针(或引用)

        非类型模板参数是有类型限制的。一般而言,它可以是常整数(包括enum枚举类型)或者指向外部链接对象的指针(或引用)。

//①浮点数和类对象(class-type)不允许作为非类型模板参数:
template<double VAL>            // ERROR: 浮点数不可作为非类型模板参数
double process(double v)
{
    return v * VAL;
}
template<std::string name>      // ERROR:类对象不能作为非类型模板参数
class MyClass
{
}
//②稍作变通,我们即可使编译通过:
template<double* PVAL>
double process(const double& x)
{
    return x * (*PVAL);
}
template<const char* name>
class MyClass
{
    ...
}
③这样可顺利通过编译,但如果想在当前文件中使用这两个模板,还需要动一些手脚:

double val = 10;
double res = process<&val>(20);     // ERROR: 表达式必须含有常量值

MyClass<"hello"> x;                 // ERROR: 模板参数不能引用非外部实体

const char* s  = "hello";
MyClass<s> x;                       // ERROR: 表达式必须含有常量值

④这里点出另外一点注意事项,也就是非类型模板参数的限制,非类型模板参数可以是指针,但该指针必须指向外部链接对象。还记得在A.cpp中如何引用B.cpp中的全局变量吗,在A.hpp中使用extern关键字对外部变量加以引用。

// B.cpp
double val = 3.14159265;
char str[] = "hello";
// A.hpp
extern double val;
extern char str[];
// A.cpp
#include "A.hpp"
double res = process<&val>(10);
MyClass<str> x;

非类型模板参数限制——不可以使用内部链接对象

template <char const* name>
class MyClass {
…
};
char const* s = "hello";
MyClass<s> x; // ERROR: s is pointer to object with internal linkage
这里"hello"是个内部链接(internal linkage)对象
但是:
template <char const* name>
class MyClass {
…
};
extern char const s[] = "hello";
MyClass<s> x; // OK

①"hello"是字符串常量,因为不是“变量”,所以没有内部、外部链接属性。有内部外部链接属性的是那个s。

C++规定,有const修饰的变量,不但不可修改,还都将具有内部链接属性,也就是只在本文件可见(这是原来C语言的static修饰字的功能,现在const也有这个功能了)。又补充规定,extern const联合修饰时,extern将压制const这个内部链接属性。于是,extern char cosnt s[]将仍然有外部链接属性,但是还是不可修改的。

③char const* s = "hello";

   MyClass<s> x; // ERROR: s is pointer to object with internal linkage

   我不知道lz标注的error信息是怎样得到的,在vs2005下的错误信息是:

   invalid template argument for 'MyClass', expected compile-time constant expression

   主要原因在于template是在编译时就生成的,而s是一个指针变量,它的值是运行时可知。

④nontype template parameters要求是在编译或者链接时值必须是可知的。对于内部链接对象来说,对于其他编译单元不可见,而在编译期至少模板的实例化是共享的,但是如果内部链接对象可以作为template argument(模板实参)会发生语义上的错误。例如字符串变量(如“abcx”),它传递给模板的是个指针,而非它的值("abcx"),况且如果还有另外一个"abcx"的话,也是传递的是地址,并且这两个地址从理论上说是不相同的(即使某些编译器会做优化,让其相同)。☆但同时我们要说两个字符串变量的值是相同的,应该共享一个类的定义(注:这是从我们使用者的角度来看),而实际上编译器无法以值的方式传递这种常量,而以地址的方式传递常量。这样如果编译器生成了不同的类的实现,这就违法了模板给我的信息(相同值的非类型模板参数生成的对象是共享一个模板实例的类的),因此编译器不应该生成相应的类的实现。




 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值