[C++11] 可变参数模板

news2024/11/14 4:58:57


文章目录

  • 基本语法及原理
    • 可变参数模板的基本语法
      • 参数包的两种类型
      • 可变参数模板的定义
    • `sizeof...` 运算符
    • 可变参数模板的实例化原理
    • 可变参数模板的意义
  • 包扩展
    • 包扩展的基本概念
    • 包扩展的实现原理
    • 编译器如何展开参数包
      • 包扩展的高级应用
  • `emplace` 系列接口
    • `emplace_back` 和 `emplace` 的作用和接口定义
    • `emplace` 系列接口的优势
    • `emplace_back` 的使用示例
    • `emplace_back` 内部实现分析
      • `ListNode` 节点类的实现
      • `emplace_back` 和 `insert` 的实现
      • 完美转发的作用
    • 编译器生成的代码

基本语法及原理

C++11引入了可变参数模板(Variadic Templates),使得我们可以定义参数数量可变的模板。可变参数模板广泛应用于泛型编程中,让开发者能够编写更加灵活和通用的代码。可变参数模板支持零或多个参数,极大地提升了模板的扩展性。


可变参数模板的基本语法

在C++11之前,为了实现不同数量的参数支持,必须针对不同数量的参数编写多个重载版本的函数或类模板。C++11提供了可变参数模板语法,允许开发者编写参数数量不定的模板函数和模板类。

参数包的两种类型

可变参数模板中的参数被称为参数包(Parameter Pack)。在C++11中,有两种参数包:

  1. 模板参数包:表示零或多个模板参数,使用 class...typename... 关键字声明。
  2. 函数参数包:表示零或多个函数参数,使用类型名后跟 ... 表示。
template <class ...Args> void Func(Args... args) {}
template <class ...Args> void Func(Args&... args) {}
template <class ...Args> void Func(Args&&... args) {}

可变参数模板的定义

下面给出一个简单的可变参数模板的定义示例:

template <class ...Args> 
void Func(Args... args) {}

template <class ...Args> 
void Func(Args&... args) {}

template <class ...Args> 
void Func(Args&&... args) {}

在上面的代码中:

  • Args... 是一个模板参数包,表示零个或多个类型参数。
  • args... 是一个函数参数包,对应零个或多个形参对象。

函数参数包可以用左值引用(Args&...)或右值引用(Args&&...)的形式表示,允许参数通过引用传递,从而符合C++的引用折叠规则。通过这些形式,我们可以灵活地处理传入的不同数量和类型的参数。

sizeof... 运算符

sizeof... 是一个操作符,用于计算参数包中参数的数量。它可以直接应用于模板参数包或函数参数包,返回参数包中包含的元素数量。以下是一个示例代码:

#include <iostream>
#include <string>
using namespace std;

template <class ...Args>
void Print(Args&&... args) {
    cout << sizeof...(args) << endl;
}

int main() {
    double x = 2.2;
    Print();                          // 包中有0个参数
    Print(1);                         // 包中有1个参数
    Print(1, string("xxxxx"));        // 包中有2个参数
    Print(1.1, string("xxxxx"), x);   // 包中有3个参数
    return 0;
}

该代码示例中,通过调用 sizeof...(args) 运算符,我们可以看到传入 Print 函数的参数数量。

可变参数模板的实例化原理

从编译的角度来看,可变参数模板的本质是在编译过程中,根据参数的数量和类型,实例化出多个函数版本。例如,上述示例中的 Print 函数调用,编译器会自动生成以下四个函数:

double x = 2.2;
Print();                          // 包中有0个参数
Print(1);                         // 包中有1个参数
Print(1, string("xxxxx"));        // 包中有2个参数
Print(1.1, string("xxxxx"), x);   // 包中有3个参数

void Print();                                           // 0个参数
void Print(int&& arg1);                                 // 1个参数
void Print(int&& arg1, string&& arg2);                  // 2个参数
void Print(double&& arg1, string&& arg2, double& arg3); // 3个参数

这样,编译器会在调用处生成特定版本的函数。这种自动生成函数的方式,极大地简化了编写支持多个参数数量的函数的工作量。

可变参数模板的意义

在没有可变参数模板的情况下,我们需要通过写多个重载的函数模板来支持不同数量的参数:

void Print(); // 没有参数

template <class T1>
void Print(T1&& arg1);

template <class T1, class T2>
void Print(T1&& arg1, T2&& arg2);

template <class T1, class T2, class T3>
void Print(T1&& arg1, T2&& arg2, T3&& arg3);

随着参数数量的增加,这种做法不仅繁琐,代码的可维护性也很差。通过可变参数模板,编译器可以自动生成相应数量和类型的函数版本,进一步解放了开发者的精力,使泛型编程更加灵活。

包扩展

在C++11中,可变参数模板不仅可以处理可变数量的参数,还支持对参数包进行“扩展”操作。包扩展允许我们分解参数包中的各个元素,并为每个元素应用某种模式,从而对其进行逐个处理。包扩展为模板元编程带来了极大的灵活性,使得我们可以编写简洁、高效的代码来处理不定数量的参数。

本文将深入探讨包扩展的概念、使用方法和实现原理。

包扩展的基本概念

对于一个参数包,我们可以:

  1. 计算参数包的元素数量(使用 sizeof... 操作符)。
  2. 进行包扩展,将参数包的元素逐个展开,并应用指定的模式。

在包扩展中,我们通过在模式的右边放置一个省略号(...)来触发扩展操作。扩展操作会将参数包逐个展开并应用模式,生成一个参数列表。例如,Args... args 表示参数包 Args... 被逐个展开为一个个单独的参数,供函数逐个处理。

包扩展的实现原理

包扩展的实现依赖于编译时递归调用和模式匹配。以下代码示例展示了如何通过包扩展实现对参数包中每个元素的打印:

#include <iostream>
#include <string>
using namespace std;

// 递归终止条件:没有参数时停止递归
void ShowList() {
    cout << endl;
}

// 递归展开参数包
template <class T, class ...Args>
void ShowList(T x, Args... args) {
    cout << x << " ";     // 打印当前参数
    ShowList(args...);     // 递归调用,展开剩余参数
}

// 主调用接口,将参数包传给ShowList处理
template <class ...Args>
void Print(Args... args) {
    ShowList(args...);
}

int main() {
    Print();                              // 输出:空行
    Print(1);                             // 输出:1
    Print(1, string("xxxxx"));            // 输出:1 xxxxx
    Print(1, string("xxxxx"), 2.2);       // 输出:1 xxxxx 2.2
    return 0;
}

在上述代码中:

  1. 递归终止条件void ShowList() 函数定义为空函数,当参数包为空时调用它,从而终止递归。
  2. 递归展开参数包ShowList(T x, Args... args) 接受第一个参数 x,打印它,然后递归调用 ShowList(args...) 继续处理剩余的参数。

通过递归展开,Print 函数会依次打印每个参数,实现了包扩展。

编译器如何展开参数包

编译器在遇到包扩展时,会将参数包逐个展开为独立的参数并生成相应的函数调用。例如,以下代码调用 Print(1, string("xxxxx"), 2.2); 会展开为:

void ShowList(int x, string y, double z) {
    cout << x << " ";
    ShowList(y, z);
}

依次展开后,每次递归调用的 ShowList 函数都会处理一个参数,直到最后一个参数被处理完。

包扩展的高级应用

C++11 支持更复杂的包扩展,可以直接将参数包依次展开,并作为实参传递给另一个函数。例如,假设我们有一个 GetArg 函数,用于处理单个参数,并将其返回。我们可以使用包扩展,将参数包的每个元素都传递给 GetArg 处理,并将结果传给另一个函数 Arguments

以下代码展示了这种高级的包扩展应用:

#include <iostream>
#include <string>
using namespace std;

template <class T>
const T& GetArg(const T& x) {
    cout << x << " ";
    return x;
}

template <class ...Args>
void Arguments(Args... args) {
    // 空函数,不做实际操作,仅用于接受展开后的参数
}

template <class ...Args>
void Print(Args... args) {
    // 使用包扩展,将每个参数传递给GetArg处理,结果传给Arguments
    Arguments(GetArg(args)...);
    // Arguments(GetArg(x), GetArg(y), GetArg(z));
}

int main() {
    Print(1, string("xxxxx"), 2.2);    // 输出:1 xxxxx 2.2
    return 0;
}

在这段代码中:

  1. GetArg 是一个函数模板,用于打印并返回每个参数。
  2. Print 中,GetArg(args)... 会将参数包 args... 依次传递给 GetArg 函数,并将 GetArg 的返回值传递给 Arguments,相当于利用Arguments这个空函数然后使用实际要将参数包各个参数传递给的函数GetArg(),然后实际上编译时的GetArg(args)...会变为:_**<font style="color:rgb(160,161,167);">Arguments(GetArg(x), GetArg(y), GetArg(z));</font>**_
  3. 编译器会在编译时生成以下代码来完成包扩展:
void Print(int x, string y, double z) {
    Arguments(GetArg(x), GetArg(y), GetArg(z));
}

emplace 系列接口

C++11 为 STL 容器引入了 emplace 系列接口,例如 emplace_backemplace,这些接口大幅提升了插入效率,尤其是在避免不必要的临时对象创建和拷贝构造方面。相比传统的 push_backinsertemplace 系列允许在容器的内存空间上直接构造对象,减少了资源消耗。


emplace_backemplace 的作用和接口定义

emplace_backemplace 的作用是直接在容器空间中构造对象,避免了拷贝或移动构造。它们接受一组参数,通过可变参数模板实现:

template <class... Args>
void emplace_back(Args&&... args);

template <class... Args>
iterator emplace(const_iterator position, Args&&... args);
  • emplace_back 接口将对象插入到容器末尾。
  • emplace 接口允许在容器的指定位置插入对象。

这两个接口都使用了可变参数模板 Args&&... args,可以接受任意数量的构造参数,使得在某些情况下比 push_backinsert 更高效。

emplace 系列接口的优势

emplace 系列的优势在于它可以避免创建临时对象。举个例子,假设我们有一个类型 T 和一个容器 container<T>,通过 emplace 接口,我们可以直接将构造 T 所需的参数传递给容器,直接在容器内存中构造 T 对象。这样减少了对象拷贝的需求,尤其在构造复杂对象时效率更高。

emplace_back 的使用示例

以下示例代码展示了 emplace_back 的不同用法:

#include <list>
#include <string>
#include <iostream>

using namespace std;

int main() {
    list<string> lt;

    // 传递左值,类似于 push_back,会调用拷贝构造
    string s1("111111111111");
    lt.emplace_back(s1);

    // 传递右值,类似于 push_back,会调用移动构造
    lt.emplace_back(move(s1));

    // 直接传递构造 string 的参数,emplace_back 在容器空间直接构造对象
    lt.emplace_back("111111111111");

    list<pair<string, int>> lt1;

    // 构造 pair 并拷贝/移动到 list 节点
    pair<string, int> kv("苹果", 1);
    lt1.emplace_back(kv);  // 拷贝
    lt1.emplace_back(move(kv)); // 移动

    // 直接传递构造 pair 的参数,在容器空间直接构造 pair 对象
    // 将参数包直接向下传,一直传到pair的构造,然后在对象的位置直接构造
    lt1.emplace_back("苹果", 1);

    return 0;
}

在上面的代码中,我们演示了 emplace_back 的三种使用方式:

  1. 传入左值参数(s1),进行拷贝构造。
  2. 传入右值参数(move(s1)),进行移动构造。
  3. 直接传递构造 stringpair 的参数,在容器内存中直接构造对象。

emplace_back 内部实现分析

为了理解 emplace 系列接口的实现,我们在给定代码中模拟了 list 容器的 emplace_backinsert 方法的实现。这些方法使用了可变参数模板和完美转发,确保参数类型的精确传递。

ListNode 节点类的实现

ListNode中,通过模板构造函数实现直接构造数据类型 T 的对象,避免了额外的拷贝:

template <class T>
struct ListNode {
    ListNode<T>* _next;
    ListNode<T>* _prev;
    T _data;

    // 移动构造
    ListNode(T&& data)
    : _next(nullptr), _prev(nullptr), _data(move(data)) {}

    // 可变参数模板构造函数
    template <class... Args>
    ListNode(Args&&... args)
    : _next(nullptr), _prev(nullptr), _data(std::forward<Args>(args)...) {}
};

在这里,我们定义了 ListNode 的两种构造方式:

  1. 移动构造函数,用于右值参数。
  2. 可变参数模板构造函数,通过 std::forward<Args>(args)... 完美转发参数,实现对象的直接构造。

emplace_backinsert 的实现

emplace_back 是通过调用 insert 来实现的,而 insert 则会根据传递的参数类型直接在节点位置构造对象 T

template <class T>
class list {
    typedef ListNode<T> Node;

public:
    template <class... Args>
    void emplace_back(Args&&... args) {
        insert(end(), std::forward<Args>(args)...);
    }

    template <class... Args>
    iterator insert(iterator pos, Args&&... args) {
        Node* cur = pos._node;
        Node* newnode = new Node(std::forward<Args>(args)...); // 直接构造新节点
        Node* prev = cur->_prev;

        // 插入节点到链表
        prev->_next = newnode;
        newnode->_prev = prev;
        newnode->_next = cur;
        cur->_prev = newnode;

        return iterator(newnode);
    }

private:
    Node* _head;
};
  • emplace_back 将参数包传递给 insert 方法。
  • insert 接受位置迭代器 pos 和一组可变参数,并通过 std::forward<Args>(args)... 将参数完美转发给 Node 构造函数。
  • insert 方法直接在链表节点位置构造对象,避免了不必要的拷贝操作。

完美转发的作用

传递参数包过程中,如果是<font style="color:rgb(31,35,41);"> Args&&... args </font>的参数包,要⽤完美转发参数包,⽅式如下<font style="color:rgb(31,35,41);">std::forward<Args>(args)... </font>,否则编译时包扩展后右值引⽤变量表达式就变成了左值。

emplace_back 的实现中,我们使用了 std::forward<Args>(args)...。完美转发确保参数类型保持不变(左值或右值),而不受函数调用的影响。如果不使用 std::forward,右值引用参数在传递过程中会被转换为左值引用,从而无法实现高效的移动语义。

编译器生成的代码

在实际编译过程中,编译器会根据传入的参数类型为 emplace_backinsert 生成适当的重载版本。例如,对于以下代码:

lt.emplace_back("111111111111");

编译器会自动生成以下版本的 emplace_back 函数:

void emplace_back(const char* s) 
{
    insert(end(), std::forward<const char*>(s));
}

每当传入不同参数组合,编译器会生成相应的重载函数,以实现高效的对象构造。

模拟实现的整体List.h:

// List.h
namespace bit
{
	template<class T>
	struct ListNode
	{
		ListNode<T>* _next;
		ListNode<T>* _prev;
		T _data;
		ListNode(T&& data)
			:_next(nullptr)
			, _prev(nullptr)
			, _data(move(data))
		{}
		template <class... Args>
		ListNode(Args&&... args)
			: _next(nullptr)
			, _prev(nullptr)
			, _data(std::forward<Args>(args)...)
		{}
	};

	template<class T, class Ref, class Ptr>
	struct ListIterator
	{
		typedef ListNode<T> Node;
		typedef ListIterator<T, Ref, Ptr> Self;
		Node* _node;
		ListIterator(Node* node)
			:_node(node)
		{}

		// ++it;
		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}

		Self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}

		Ref operator*()
		{
			return _node->_data;
		}

		bool operator!=(const Self& it)
		{
			return _node != it._node;
		}
	};

	template<class T>
	class list
	{
		typedef ListNode<T> Node;

	public:
		typedef ListIterator<T, T&, T*> iterator;
		typedef ListIterator<T, const T&, const T*> const_iterator;

		iterator begin()
		{
			return iterator(_head->_next);
		}

		iterator end()
		{
			return iterator(_head);
		}

		void empty_init()
		{
			_head = new Node();
			_head->_next = _head;
			_head->_prev = _head;
		}

		list()
		{
			empty_init();
		}

		void push_back(const T& x)
		{
			insert(end(), x);
		}

		void push_back(T&& x)
		{
			insert(end(), move(x));
		}

		iterator insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* newnode = new Node(x);
			Node* prev = cur->_prev;

			// prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;

			return iterator(newnode);
		}

		iterator insert(iterator pos, T&& x)
		{
			Node* cur = pos._node;
			Node* newnode = new Node(move(x));
			Node* prev = cur->_prev;

			// prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;

			return iterator(newnode);
		}

		template <class... Args>
		void emplace_back(Args&&... args)
		{
			insert(end(), std::forward<Args>(args)...);
		}

		// 原理:本质编译器根据可变参数模板⽣成对应参数的函数
		/*void emplace_back(string& s)
		{
		insert(end(), std::forward<string>(s));
		}
		void emplace_back(string&& s)
		{
		insert(end(), std::forward<string>(s));
		}
		void emplace_back(const char* s)
		{
		insert(end(), std::forward<const char*>(s));
		}
		*/

		template <class... Args>
		iterator insert(iterator pos, Args&&... args)
		{
			Node* cur = pos._node;
			Node* newnode = new Node(std::forward<Args>(args)...);
			Node* prev = cur->_prev;

			// prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;

			return iterator(newnode);
		}

	private:
		Node* _head;
	};
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2236944.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Axure设计之左右滚动组件教程(动态面板)

很多项目产品设计经常会遇到左右滚动的导航、图片展示、内容区域等&#xff0c;接下来我们用Axure来实现一下左右滚动的菜单导航。通过案例我们可以举一反三进行其他方式的滚动组件设计&#xff0c;如常见的上下滚动、翻页滚动等等。 一、效果展示&#xff1a; 1、点击“向左箭…

qt QListWidget详解

1、概述 QListWidget 是 Qt 框架中的一个类&#xff0c;它提供了一个基于模型的视图&#xff0c;用于显示项目的列表。QListWidget 继承自 QAbstractItemView 并为项目列表提供了一个直观的接口。与 QTreeView 和 QTableView 不同&#xff0c;QListWidget 是专门为单行或多行项…

vue--vueCLI

何为CLI ■ CLI是Command-Line Interface,俗称脚手架. ■ 使用Vue.js开发大型应用时&#xff0c;我们需要考虑代码目录结构、项目结构和部署、热加载、代码单元测试等事情。&#xff08;vue 脚手架的作用&#xff09;&#xff0c; 而通过vue-cli即可&#xff1a;vue-cli 可以…

思维,CF 1735D - Meta-set

目录 一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 二、解题报告 1、思路分析 2、复杂度 3、代码详解 一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 1735D - Meta-set 二、解题报告 1、思路分析 考虑一个五元组<a, b, c…

C#的6种常用集合类

一.先来说说数组的不足&#xff08;也可以说集合与数组的区别&#xff09;&#xff1a; 1.数组是固定大小的&#xff0c;不能伸缩。虽然System.Array.Resize这个泛型方法可以重置数组大小&#xff0c;但是该方法是重新创建新设置大小的数组&#xff0c;用的是旧数组的元素初始…

深度学习-神经网络基础-激活函数与参数初始化(weight, bias)

一. 神经网络介绍 神经网络概念 神经元构建 神经网络 人工神经网络是一种模仿生物神经网络结构和功能的计算模型, 由神经元构成 将神经元串联起来 -> 神经网络 输入层: 数据 输出层: 目标(加权和) 隐藏层: 加权和 激活 全连接 第N层的每个神经元和第N-1层的所有神经元…

SpringBoot框架在资产管理中的应用

3系统分析 3.1可行性分析 通过对本企业资产管理系统实行的目的初步调查和分析&#xff0c;提出可行性方案并对其一一进行论证。我们在这里主要从技术可行性、经济可行性、操作可行性等方面进行分析。 3.1.1技术可行性 本企业资产管理系统采用Spring Boot框架&#xff0c;JAVA作…

【C#】选课程序增加、删除统计学时

文章目录 【例6-2】编写选课程序。利用利用列表框和组合框增加和删除相关课程&#xff0c;并统计学时数1. 表6-2 属性设置2. 设计窗体及页面3. 代码实现4. 运行效果 【例6-2】编写选课程序。利用利用列表框和组合框增加和删除相关课程&#xff0c;并统计学时数 分析&#xff1…

uniapp—android原生插件开发(1环境准备)

本篇文章从实战角度出发&#xff0c;将UniApp集成新大陆PDA设备RFID的全过程分为四部曲&#xff0c;涵盖环境搭建、插件开发、AAR打包、项目引入和功能调试。通过这份教程&#xff0c;轻松应对安卓原生插件开发与打包需求&#xff01; 项目背景&#xff1a; UniApp集成新大陆P…

ProtoBuf实战之网络版通讯录

目录 网络版通讯录需求 实现网络版通讯录 搭建服务端客户端 协议约定 客户端菜单功能 服务端代码 Protobuf 还常用于 通讯协议、服务端数据交换 的场景&#xff0c;接下来&#xff0c;我们将实现一个网络版本的通讯录&#xff0c;模拟实现客户端与服务端的交互&#xff0…

动态规划理论基础和习题【力扣】【算法学习day.26】

前言 ###我做这类文档一个重要的目的还是给正在学习的大家提供方向&#xff08;例如想要掌握基础用法&#xff0c;该刷哪些题&#xff1f;&#xff09;我的解析也不会做的非常详细&#xff0c;只会提供思路和一些关键点&#xff0c;力扣上的大佬们的题解质量是非常非常高滴&am…

Sqoop学习

目录 一、Soop简介 二、Sqoop的安装 1. 上传压缩包到/opt/install目录下 2.解压 3.修改文件名 4.拷贝sqoop-1.4.7.bin__hadoop-2.6.0目录下的sqoop-1.4.7.jar包到/opt/soft/sqoop147目录下 5.拷贝sqoop-1.4.7.bin__hadoop-2.6.0/lib目录下该jar包到sqoop/lib目录下 6.复…

关于随身wifi,看了再决定要不要买!2024年最受欢迎的随身wifi品牌推荐!

话费、流量费缴纳起来肉疼&#xff0c;毕竟不是每个月都有很大需求&#xff0c;主打一个该省省该花花。特别是短租人群、在校学生、出差或旅游的人群、追求高性价比的人群&#xff0c;随身Wifi特别实用&#xff0c;出门当WiFi&#xff0c;在家当宽带&#xff0c;两不耽误&#…

[vulnhub] DarkHole: 1

https://www.vulnhub.com/entry/darkhole-1,724/ 端口扫描主机发现 探测存活主机&#xff0c;184是靶机 nmap -sP 192.168.75.0/24 Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-11-08 09:59 CST Nmap scan report for 192.168.75.1 Host is up (0.00027s latency). MA…

iPhone 微信传大文件到QQ

问题 解决方法 在微信里打开要拷贝的文件 选择“...” 选择“用其他应用打开” 长按QQ 选择“拷贝到App”&#xff08;有些版本是“在App中打开”&#xff09;

【学习笔记】网络设备(华为交换机)基础知识 11 —— 信息中心 ② 配置案例

提示&#xff1a;学习华为交换机信息中心配置案例&#xff0c;包含配置输出Log信息 &#xff08; 输出到Log缓冲区、控制台、日志文件、终端 &#xff09;、配置输出Trap信息 &#xff08; 输出到Trap缓冲区、控制台、日志文件、终端 &#xff09;、 配置输出Debug信息 &#x…

电脑管家实时监控软件下载 | 六款知名又实用的电脑监控软件推荐!(珍藏篇)

在当今的商业环境&#xff0c;企业对于员工在工作期间的行为监控需求越来越强烈。 尤其是在网络化和信息化程度不断提高的今天&#xff0c;电脑管家实时监控软件是企业管理员工工作行为、提高工作效率、防止信息泄露的重要工具。 本文&#xff0c;将为您推荐六款知名又实用的电…

PICO+Unity MR空间网格

官方链接&#xff1a;空间网格 | PICO 开发者平台 注意&#xff1a;该功能只能打包成APK在PICO 4 Ultra上真机运行&#xff0c;无法通过串流或PICO developer center在PC上运行。使用之前要开启视频透视。 在 Inspector 窗口中的 PXR_Manager (Script) 面板上&#xff0c;勾选…

Spring Boot中集成MyBatis操作数据库详细教程

目录 前言1. 项目依赖配置1.1 引入MyBatis和数据库驱动依赖1.2 数据源配置 2. 创建数据库映射实体类3. 创建Mapper层接口4. 创建Service层4.1 定义Service接口4.2 实现Service接口 5. 创建Controller层6. 运行和测试项目6.1 启动项目6.2 测试接口 7. 总结 前言 在Java开发中&a…

DirectShow过滤器开发-写AVI视频文件过滤器

下载本过滤器DLL 本过滤器将视频流和音频流写入AVI视频文件。 过滤器信息 过滤器名称&#xff1a;写AVI 过滤器GUID&#xff1a;{2EF49957-37DF-4356-A2A0-ECBC52D1984B} DLL注册函数名&#xff1a;DllRegisterServer 删除注册函数名&#xff1a;DllUnregisterServer 过滤器有…