C/C++编程:模板

1059 篇文章 278 订阅

外部模板

  • 传统C++中,模板只有在使用时才会被编译器实例化。即只要在每个编译单元(文件)中编译的代码中遇到了被完整定义的模板,都会实例化。

  • 这就产生了重复实例化而导致编译时间增加,并且,我们没有办法通知编译器不要触发模板实例化。

为此,C++11引入了外部模板,扩充了原来的强制编译器在特定位置实例化模板的语法,使我们能够显式的通知编译器何时进行模板的实例化

  • template class std::vector<bool>; // 强行实例化
  • extern template class std::vector<int>; // 不在该当前编译文件中实例化模板

完整例子:

#include <iostream>
#include <vector>

template class std::vector<bool>;           // forcely instantiation
extern template class std::vector<double>;  // external template for avoiding instantiation in this file

template<bool T> class MagicType {
    bool magic = T;
};

int main() {
    // the >> in template
    std::vector<std::vector<int>> matrix;
    std::vector<MagicType<(1>2)>> magic; // legal, but not recommended
}

尖括号>

在传统C++的编译器中,>>一律被当作右移运算符来进行处理。但实际上我们很容易就会写出嵌套模板的代码:

std::vector<std::vector<int>> matrix;

这在传统C++编译器下是不能被编译的,而C++11开始,连续的尖括号将变得合法,并且能够顺序通过编译。比如下面的:

template<bool T>
class MagicType{
	bool magic == T;
};

std::vector<MagicType<<1>2>> magic// 合法,但不建议写出这样的代码

类型别名模板

在了解类型别名模板之前,需要理解模板类型之间的不同。

  • 模板是用来产生类型的
  • 在传统C++中,typedef可以为类型定义一个新的名称,但是却没有办法为模板定义一个新的名称。因为,模板不是类型:
#include <vector>

template<typename  T, typename  U>
class MagincType{
public:
    T dark;
    U magic;
};

// 不合法
template<typename  T>
typedef MagincType<std::vector<T>, std::string> FakeDarkMagic;

C++11使用using引入了下面这种形式的写法:


#include <iostream>
#include <algorithm>
#include <vector>

template<typename  T, typename  U>
class MagincType{
public:
    T dark;
    U magic;
};


template<typename  T>
using TrueDarkMagic = MagincType<std::vector<T>, std::string>;

int  main()
{
    TrueDarkMagic<bool> you;
}

默认模板参数

定义一个加法参数如下:

template<typename T, typename U>
auto add(T x, U y) -> decltype(x+y) {
    return x+y;
}

// 调用模板参数
auto ret = add<int, int>(1, 3);

也就是说,我们如果要使用这个模板,就必须每次都指定模板参数的类型

在C++11中提供了一种便利,可以指定模板的默认参数:

template<typename T = int, typename U = int>
auto add(T x, U y) -> decltype(x+y) {
    return x+y;
}

// 调用模板参数
auto ret = add(1, 3);

变长参数模板

在C++11之前,无论是类模板还是函数模板,都只能按照其指定的样子,接收一组固定数量的模板参数;从C++11起,加入新的表达方法:

  • 允许任意个数、任意类别的模板参数,同时也不需要在定义时将参数的个数固定

template<typename … Ts> class Magic;

模板类Magic的对象,能够接受不受限制个数的typename作为模板的形式参数,比如下面:

class Magic<int,
			std::vector<int>,
			std::map<std::string,
			std::vector<int>>> darkMagic

当然也可以个数为0的模板参数:

class Magic<> nothing;
  • 如果不希望产生的模板参数个数为0,可以手动的定义至少一个模板参数:

template<typename Require, typename … Ts> class Magic;

  • 变长参数模板也能直接调整到模板函数上。传统C中的printf函数,虽然也能达成不定个数的形参的调用,但是其并非类型安全。而C++11除了定义类型安全的变长参数函数外,还可以使类似printf的函数能自然地处理非自带类别的对象。除了在模板参数中能使用…表示不定长模板参数外,函数参数也使用同样的表示法代表不定长参数,这也为我们简单编写变长参数函数提供了便携的手段,比如:

template<typename … Args> void printf(const std::string &str, Args… args);

我们定义了变长的模板参数,那么如何对参数进行解包呢?

首先,我们可以使用sizeof...来计算参数的个数:

template<typename ...Ts>
void magic(Ts... args){
	std::cout << sizeof...(args) << std::endl;
}

我们可以传递任意个参数个magic函数:

magic(); //输出0
magic(1);  //输出1
magic(1, ""); //输出2

其次,对参数进行解包,到目前为止还没有一种简单的方法能够处理参数包,但是有两种经典的处理方法:

递归模板函数

递归是最经典的处理方法。这种方法不断递归的向函数传递模板参数,进而达到递归遍历所有模板参数的目的:

#include <iostream>
template<typename T0>
void printf1(T0 value){
    std::cout << value << "\n";
}

template<typename T, typename... Ts>
void printf1(T value, Ts... args){
    std::cout << value << "**\n";
    printf1(args...);
}

int main(){
    printf1(1, 2, "123", 1.2);
    return 0;
}

在这里插入图片描述

变参模板展开

当然,更好的方法是C++17中支持的变参模板参数展开的支持,这样我们就可以在一个函数中完成printf的编写

template<typename T, typename... Ts>
void printf1(T value, Ts... args){
    std::cout << value << "\n";
    if constexpr (sizeof...(args) > 0){
        printf1(args...);
    }
}

事实上,有时我们虽然使用了变参模板,但不一定要对参数做逐个遍历,我们可以利用std::bind以及完美转发等特性实现对函数和参数的绑定,从而达到成功调用的目的

初始化列表展开

递归模板函数是一种标准的做法,但缺点显而易见的在于必须定义一个终止递归的函数

这里介绍一种使用初始化列表展开的黑魔法

template<typename T, typename... Ts>
void printf1(T value, Ts... args){
    std::cout << value << "\n";
    (void) std::initializer_list<T>{([&args]{
        std::cout << args << "\n";
    }(),value)...};
}

在这个代码中,额外使用了C++11中提供的初始化列表以及lambda表达式的特性。

通过初始化列表,(lamda表达式, value)...将会被展开。由于逗号表达式的出现,首先会执行前面的lambda表达式,完成参数的输出。为了避免编译器警告,我们可以将std::initializer_list显式转为void

折叠表达式

C++17将变长参数这种特性进一步带给了表达式:

#innclude<iostream>

template<typename... T>
auto sum(T... t){
	return (t + ...);
}

int main(){
	std::cout << sum(1, 2, 3, 4, 5, 6, 7, 8, 9);
}

非类型模板参数推导

前面我们主要提及的是模板参数的一种形式:类型模板参数

template<typename T, typename U>
auto add(T t, U u){
	return t + u;
}

其中模板的参数T和U为具体的类型。但是还有一种常见的模板参数形式可以让不同字面量称为模板参数,即非类型模板参数:

template<typename T, int BufSize>
class buffer_t{
public:
    T & alloc();
    void free(T & item);

private:
    T data[BufSize];
};


buffer_t<int , 100> buf; // 100 作为模板参数

在这种模板参数的形式下,我们可以将100作为模板的参数传递。 在C++11引入了类型推导这一特性后,我们会很自然的问,既然这里的模板参数以具体的字面量进行传递,能否让编译器辅助我们进行类型推导,通过使用占位符auto从而不再需要明确指明类型?C++11引入了这一特性:

template<auto value> void foo(){
	std::cout << value << "\n";
	return;
}

int main(){
	foo<10>(); //value被推导为int类型
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值