C++17之占位符类型作为模板参数

30 篇文章 17 订阅

因为c++ 17可以使用占位符类型(auto和decltype(auto))作为非类型模板参数类型。这意味着,我们可以为不同类型的非类型参数编写通用代码。

1. auto作为模板参数

从c++ 17起,可以使用auto来声明一个非类型模板参数。例如:

template<auto N> class S {
...
};

这允许我们为不同类型实例化非类型模板参数N:

S<42> s1; // OK: type of N in S is int
S<'a'> s2; // OK: type of N in S is char

但是,不能使用此功能来获取通常不允许作为模板参数的类型的实例化:

S<2.5> s3; // ERROR: template parameter type still cannot be double

我们甚至可以有一个特定的类型作为部分特化:

template<int N> class S<N> 
{
...
};

甚至类模板参数推断也受到支持,例如:

template<typename T, auto N>
class A 
{
public:
    A(const std::array<T,N>&) 
    {
    }

    A(T(&)[N]) 
    {
    }

    ...
};

这个类可以推导出T的类型,N的类型,N的值:

A a2{"hello"}; // OK, deduces A<const char, 6> with N being int
std::array<double,10> sa1;
A a1{sa1}; // OK, deduces A<double, 10> with N being std::size_t

还可以限定auto,例如,要求模板参数的类型为指针:

template<const auto* P> struct S;

通过使用可变参数模板,可以参数化模板,使用异构常量模板参数列表:

template<auto... VS> class HeteroValueList 
{
};

或同构常量模板参数列表:

template<auto V1, decltype(V1)... VS> class HomoValueList {
};

例如:

HeteroValueList<1, 2, 3> vals1; // OK
HeteroValueList<1, 'a', true> vals2; // OK
HomoValueList<1, 2, 3> vals3; // OK
HomoValueList<1, 'a', true> vals4; // ERROR

2. 字符和字符串参数化模板

该特性的一个应用程序是允许同时传递一个字符或字符串作为模板参数。我们可以改进使用折叠表达式输出任意数量参数的方法:

#include <iostream>
#include <string>

template<auto Sep = ' ', typename First, typename... Args>
void print(First first, const Args& ... args) 
{
    std::cout << first;
    auto outWithSpace = [](auto const& arg) 
    {
        std::cout << Sep << arg;
    };

    (..., outWithSpace(args));
    std::cout << '\n';
}

int main()
{
    std::string s{ "world" };
    print(7.5, "hello", s); // prints: 7.5 hello world
    print<'-'>(7.5, "hello", s); // prints: 7.5-hello-world
    static const char sep[] = ", ";
    print<sep>(7.5, "hello", s); // prints: 7.5, hello, world
    print<-11>(7.5, "hello", s); // prints: 7.5-11hello-11world

    return 0;
}

结果如下:

但是通过指定分隔符Sep参数化print(),现在可以显式地传递一个不同的字符作为第一个模板参数:

print<'-'>(7.5, "hello", s); // prints: 7.5-hello-world

由于使用auto,我们甚至可以传递一个字符串文字,必须声明它为一个没有链接的对象:

static const char sep[] = ", ";
print<sep>(7.5, "hello", s); // prints: 7.5, hello, world

或者我们可以传递任何其他类型的分隔符作为模板参数:

print<-11>(7.5, "hello", s); // prints: 7.5-11hello-11world

3. 元编程定义常量

模板参数自动特性的另一个应用程序是更容易地定义编译时常量。而不需如下定义:

template<typename T, T v>
struct constant
{
static constexpr T value = v;
};

using i = constant<int, 42>;
using c = constant<char, 'x'>;
using b = constant<bool, true>;

现在仅仅按照如下定义:

template<auto v>
struct constant
{
static constexpr auto value = v;
};
using i = constant<42>;
using c = constant<'x'>;
using b = constant<true>;

并且对于如下:

template<typename T, T... Elements>
struct sequence {
};
using indexes = sequence<int, 0, 3, 4>;

现在需要:

template<auto... Elements>
struct sequence {
};
using indexes = sequence<0, 3, 4>;

现在,甚至可以定义编译时对象来表示一个异构的值列表(类似于一个元组):

using tuple = sequence<0, 'h', true>;

4. decltype(auto)作为模板参数

还可以使用c++ 14中引入的另一种占位符类型decltype(auto)。但是,请注意,这种类型有非常特殊的规则来推断类型。根据decltype,如果传递的是表达式而不是名称,那么它将根据value类别推断类型:

  • type为prvalue(例如:变量)
  • type&为lvalue(例如:具有名字的对象)
  • type&&为xvalue(例如:对象转换为右值引用,如std::move())。

这意味着,可以很容易地将模板参数推断为引用,这可能会产生令人惊讶的效果。

#include <iostream>

template<decltype(auto) N>
struct S {
    void printN() const {
        std::cout << "N: " << N << '\n';
    }
};
static const int c = 42;
static int v = 42;
int main()
{
    S<c> s1; // deduces N as const int 42
    S<(c)> s2; // deduces N as const int& referring to c
    s1.printN();//prints:N:42
    s2.printN();//prints:N:42
    S<(v)> s3; // deduces N as int& referring to v
    v = 77;
    s3.printN(); // prints: N: 77
    S<11> s4;
    s4.printN();
}

结果如下:

 

ps: 转自 https://blog.csdn.net/lanchunhui/article/details/49634077

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

非类型类模板参数

这里我们使用一个新版本的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];
}

 

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

 

非类型函数模板参数

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

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

千万不要小瞧这样的简单机制(整个计算机理论的恢弘大厦的本质也还是0/1呢),如果把函数(仿函数)或者某一操作当做参数传递给某一函数,这些实现了一些简单的功能的函数将产生巨大的作用。例如,借助标准模板库(STL),可以将这个函数模板的一个实例传递给集合中的每一个元素,将集合中的每一个值都增加一个预设的值。
 

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

这里要想成为STL <algorithm>算法中某一函数的参数(尤其是在内部作为函数使用的),需要满足一定的要求,比如本例中的std::transform(),传递进来的在内部作为函数使用的第四个参数,只能接受一个参数,且需返回一个值(返回值的类型就是目标容器的元素类型),这是基本要求。

另外还有一个问题需要注意,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命名空间中

 

非类型模板参数的限制

非类型模板参数是有类型限制的。一般而言,它可以是常整数(包括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关键字对外部变量加以引用。

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值