C++模板及实战,以及重载运算符

C++ 模板函数、lambda 与运算符重载:从一个 vector 过滤器开始

今天学的是 C++ 里比较“工程化”的几个语法点:

  1. 模板函数 template<typename T>
  2. 函数参数的自动推导
  3. const T & 引用传参
  4. lambda 表达式
  5. 运算符重载 operator|
  6. | 写出类似管道的过滤操作
  7. 三目运算符与 std::common_type_t

核心代码如下:

#include<bits/stdc++.h>
template<typename T> void print(const T &vec)
{
	for(const auto &item:vec)
	{
		std::cout<<item<<'\n';
	}
}
template<typename Tu,typename Func> std::vector<Tu> operator| (const std::vector<Tu> &a,const Func &func)
{
	std::vector<Tu> res;
	for(const auto &b:a)
	{
		if(func(b)) res.emplace_back(b);
	}
	return res;
}
int main()
{
	std::vector<int> init;
	for(int i=1;i<=100;++i) init.emplace_back(i);
	auto is_even=[](int i)
	{
		return (i%2==0);
	};
	print(init|is_even);
}

这段代码的作用是:

1~100 里筛出所有偶数,然后打印。

输出大概是:

2
4
6
...
100

1. 模板函数:让函数适配多种类型

首先是这个打印函数:

template<typename T> void print(const T &vec)
{
	for(const auto &item:vec)
	{
		std::cout<<item<<'\n';
	}
}

这里:

template<typename T>

表示 T 是一个模板类型。

调用时,编译器会自动根据实参推导 T 是什么。

比如:

std::vector<int> a;
print(a);

那么编译器推导:

T = std::vector<int>

所以这个函数等价于被实例化成:

void print(const std::vector<int> &vec)
{
	for(const auto &item:vec)
	{
		std::cout<<item<<'\n';
	}
}

模板函数的本质不是运行时判断类型,而是编译期生成对应版本的函数


2. 为什么参数写成 const T &

void print(const T &vec)

这里有两个重点。

第一,& 表示引用传参。

如果不写引用:

void print(T vec)

那么传入一个 vector 时,会复制整个 vector

如果 vector 很大,这个复制成本就很高。

写成:

const T &vec

表示:

不复制,只引用原对象

第二,const 表示函数内部不能修改 vec

所以:

const T &vec

就是:

不拷贝,也不修改

这是读数据时很常见的写法。


3. range-based forconst auto &

代码里还有:

for(const auto &item:vec)

它的意思是:

遍历 vec 里面的每个元素。

其中:

auto

让编译器自动推导元素类型。

如果 vec 是:

std::vector<int>

那么:

item

就是 int 类型。

写成:

const auto &item

表示:

不复制元素,也不修改元素

虽然 int 复制成本很低,但如果元素是 string、结构体、类对象,用引用就更重要。


4. lambda 表达式:临时写一个函数对象

这里:

auto is_even=[](int i)
{
	return (i%2==0);
};

定义了一个 lambda。

可以理解成一个临时的小函数:

bool is_even(int i)
{
	return i%2==0;
}

但 lambda 本质上不是普通函数,而是一个编译器生成的匿名类对象。

大概可以理解成:

struct Lambda
{
	bool operator()(int i) const
	{
		return i%2==0;
	}
};

然后:

auto is_even=[](int i)
{
	return (i%2==0);
};

大概相当于:

Lambda is_even;

所以后面可以写:

is_even(2)

本质类似于:

is_even.operator()(2)

5. 重载 operator|

最关键的是这个函数:

template<typename Tu,typename Func> std::vector<Tu> operator| (const std::vector<Tu> &a,const Func &func)
{
	std::vector<Tu> res;
	for(const auto &b:a)
	{
		if(func(b)) res.emplace_back(b);
	}
	return res;
}

它重载了 | 运算符。

原本 | 是按位或,比如:

int x=3|5;

但现在我们给 std::vector<Tu> 和某个函数对象之间定义了新的含义。

当代码写:

init|is_even

编译器会把它理解成:

operator|(init,is_even)

也就是说:

init

传给第一个参数:

const std::vector<Tu> &a

而:

is_even

传给第二个参数:

const Func &func

6. 模板参数怎么推导

调用:

init|is_even

其中:

init

类型是:

std::vector<int>

所以:

const std::vector<Tu> &a

会推导出:

Tu = int

is_even 是一个 lambda 对象,所以:

Func = lambda 的匿名类型

于是这个模板函数大概实例化成:

std::vector<int> operator|(const std::vector<int> &a,const Lambda &func)
{
	std::vector<int> res;
	for(const auto &b:a)
	{
		if(func(b)) res.emplace_back(b);
	}
	return res;
}

7. func(b) 是什么

循环里:

if(func(b)) res.emplace_back(b);

其中:

func

其实就是 is_even 的引用。

所以:

func(b)

等价于:

is_even(b)

也就是判断:

b%2==0

如果是偶数,就加入结果数组:

res.emplace_back(b);

最后:

return res;

返回筛选后的新 vector


8. init|is_even 的整体过程

这句:

print(init|is_even);

可以拆成:

auto tmp=operator|(init,is_even);
print(tmp);

也就是:

std::vector<int> tmp;
for(const auto &b:init)
{
	if(is_even(b)) tmp.emplace_back(b);
}
print(tmp);

所以本质是一个 filter 操作。

如果不用运算符重载,可以写成普通函数:

template<typename Tu,typename Func> std::vector<Tu> filter(const std::vector<Tu> &a,const Func &func)
{
	std::vector<Tu> res;
	for(const auto &b:a)
	{
		if(func(b)) res.emplace_back(b);
	}
	return res;
}

调用:

print(filter(init,is_even));

而现在用 operator| 后,可以写成:

print(init|is_even);

更像“管道”。


9. emplace_backpush_back

代码里用了:

res.emplace_back(b);

对于 int 来说,emplace_back(b)push_back(b) 差别不大。

但如果元素是结构体或类对象:

struct node
{
	int x,y;
	node(int x,int y):x(x),y(y){}
};

那么:

vec.emplace_back(1,2);

可以直接在 vector 内部构造对象。

而:

vec.push_back(node(1,2));

一般是先构造临时对象,再放进 vector

所以 emplace_back 更偏工程习惯。


10. 今天还学了三目运算符和 common_type_t

三目运算符:

条件 ? 表达式1 : 表达式2

比如:

return a>b?a:b;

等价于:

if(a>b) return a;
else return b;

但三目运算符有一个重要特点:

它自己也有类型。

比如:

auto x=true?1:2.5;

虽然条件是 true,但是编译器仍然会同时看 12.5 的类型。

1int2.5double

最终共同类型是:

double

所以:

x

double


11. true?Tp{}:Up{} 是什么

在模板里有时会看到:

decltype(true?Tp{}:Up{})

它的意思是:

构造一个 Tp 类型的临时对象和一个 Up 类型的临时对象,然后放进三目运算符里,让编译器判断这两个类型混在一起后会变成什么类型。

比如:

decltype(true?int{}:double{})

结果是:

double

因为 intdouble 的共同类型是 double

这里的 true 不是重点。

重点是三目运算符会根据第二、第三个表达式推出一个统一类型。


12. std::common_type_t<Tp,Up>

更正规的方法是:

std::common_type_t<Tp,Up>

它表示:

TpUp 的共同类型。

比如:

std::common_type_t<int,double>

结果是:

double
std::common_type_t<int,long long>

结果一般是:

long long

所以可以写一个支持不同类型参数的 Max

#include<bits/stdc++.h>
template<typename Tp,typename Up>
std::common_type_t<Tp,Up> Max(const Tp &a,const Up &b)
{
	return a>b?a:b;
}
int main()
{
	auto x=Max(1,2.5);
	std::cout<<x<<'\n';
}

这里:

Max(1,2.5)

推导出:

Tp = int
Up = double

返回类型是:

std::common_type_t<int,double>

也就是:

double

13. 今天的核心收获

今天的代码可以总结成一句话:

用模板函数让代码适配多种类型,用 lambda 传入筛选规则,用运算符重载把 filter(vec,func) 写成了 vec|func

具体学到的点:

template<typename T>

表示定义模板类型。

const T &

表示引用传参,不拷贝,也不修改。

auto is_even=[](int i)
{
	return i%2==0;
};

表示定义一个 lambda 函数对象。

operator|(init,is_even)

等价于:

init|is_even

而:

func(b)

就是调用传进来的 lambda。

最后:

print(init|is_even);

看起来像一条管道:

原数组 | 筛选条件 | 输出

虽然底层仍然是普通函数调用,但写法更接近现代 C++ 的函数式风格。

posted @ 2026-06-16 12:47  dtcoke  阅读(5)  评论(0)    收藏  举报