lambda 与函数指针

C++ 的函数类型包括了以下几种:

  • 函数指针;
  • 成员函数指针;
  • 上述两种函数类型的引用、c-v-noexcept 修饰符的排列组合。

在 C++11 后,语言标准引入了更灵活的 lambda 函数;因此在函数类型中又新增了 lambda 类型和一堆修饰符的排列组合。

实际上,C++ 中 lambda 函数是一个匿名类的类对象;这个匿名类由编译器生成,并且重载了 operator()() 运算符。所以任何一个重载了 operator()() 的类对象都可以被视作是广义上的 lambda。

如果 lambda 函数带有捕获(包括引用和值捕获),那么这个生成的匿名类就会持有对应值类型的数据成员;否则匿名类是一个大小为 1 的空类(标准要求不含数据成员的类大小必须为 1)。

并且,不带有捕获的 lambda 函数可以被隐式类型转换为一个函数指针;这一行为可以通过在 lambda 表达式的捕获列表(方括号)左侧添加一个一元运算符 + 显式触发。

最重要的是,任意两个 lambda 表达式的实际类型永远不可能相同(见 wiki),即使它们的函数签名和捕获列表完全一致。

这些类型的排列组合背后是沉重的历史包袱,以至于函数类型被称作是 Abominable Function Types(糟糕的函数类型)。

这里的历史包袱说的就是 C 语言中的函数类型(呕。

在与一些需要传递自定义函数的场景下,我们往往会传递一个 lambda 或指向某个函数的指针;这种用法在与一些提供了 Modern Cpp 封装的接口交互时会显得很优雅和简洁。

这些接口往往都被编写为模板函数,使得它能够接收任意可调用类型;而在无法模板化的接口中,则使用 std::function 作为可调用类型的存储容器。

但对于一些没有提供 Modern Cpp 支持,或者干脆就是由 C 语言编写的接口(例如 Linux 的系统调用)来说,我们在传递这类可调用对象时能选择的类型就只剩下函数指针了。

函数指针很万能,但有一点致命缺陷:它只能指向一个已存在的全局函数,并且带有捕获列表的 lambda 函数无法经类型转换变成函数指针。所以如果我想传递一个指针,那么我必须在全局作用域创建一个新函数。

而且麻烦还不止于此:如果我需要的功能被封装在一个类方法中,那么我还必须想办法提供一个能在全局范围内访问到这个局部对象的接口,否则创建的全局函数什么都干不了。

因为函数指针中包含了函数的参数类型,所以这个局部对象是不能经由函数参数列表传入的;不然就会破坏参数列表。

到了这里已经可以预见一个软件工程灾难了:为了一个简单的函数指针,就需要创建一个全局函数,还得想办法将局部对象的访问权暴露给全局。

所以这里我们需要一个简洁的解决方案。

1.将局部对象的作用域提升到全局

lambda 函数与常规函数一样,能够在函数体内未经捕获地访问全局对象,前提是在定义 lambda 时就已经看到了这些对象。

这里的全局对象包括:

  • 任何被 static 修饰的变量;
  • 在全局作用域内声明/定义的变量(包括 extern 引入)。

而一个不带捕获的 lambda 是可以被隐式转换为函数指针的。因此我们可以将捕获列表内的局部对象提升为 static,然后使用 lambda 包装一下。

int main()
{static std::vector<int> arr;auto fptr = +[]( int a ) { arr.push_back( a ); };foo( fptr ); // 传递函数指针
}

这很蠢,而且局部对象被提升为 static 毫无意义,甚至会破坏原有的 RAII 语义;更不用说部分情况下全局化一个局部对象的行为可能比获得一个函数指针更困难。

2.利用全局函数

我们可以反过来做:把一个带捕获的 lambda 表达式存放到一个函数内部作用域的 static 变量中,然后透过一个能够访问这个 static 变量的函数访问。

注意:因为在标准定义中,lambda 函数的赋值运算符全部都被标记为弃置,所以我们只能通过声明语句,从一个已存在的 lambda 对象上再构造一个 lambda 对象。

这时我们需要引入一点模板元编程技巧。

template<typename Lambda>
typename std::enable_if<std::is_class<FnTp>::value && !std::is_empty<Lambda>::value>::typemake_fnptr( Lambda&& fn )
{static typename std::remove_reference<Lambda>::type fntor = std::forward<Lambda>( fn );
}

我们可以在内部返回一个不带捕获、且转为了函数指针的 lambda 函数;但我们很快就会发现:我们不知道返回的函数指针类型是什么,也就是说这个函数的返回值是未决的;这会直接触发编译失败。

我们无法推断返回类型的原因有很多:

  • 我们不知道这个 Lambda 的返回值和参数列表是什么,所以写不出能够描述指向二次包装后 lambda 的指针类型;
  • 函数参数列表需要在调用该函数时传入,但是可变模板参数列表会与函数参数中的模板参数产生语义冲突;
  • 即使我们获取了被包装的类型参数列表,我们也无法在函数体内就地展开并填充到二次包装的 lambda 中(这里需要使用模板类的模式匹配)。

第一条原因还能借由引入复杂的类型萃取器解决,第二条也可以引入额外的类型包装解决,但第三条原因直接宣判了这个方案的死刑。

3.引入一个包装器类

因为类的静态函数天然就可以被转换为函数指针,同时这个静态函数的可见范围是全局,所以我们不妨引入一个模板包装类,将返回的函数指针指向这个类的静态函数。

为了支持带有参数列表的 lambda,这个静态函数还需要被模板化。

模板函数在被实例化后(填入函数参数列表)就可以获取地址了,这里没有问题。

此外 C++ 还有一个比较有意思的语法点:返回类型都是 void 的函数,如下形式的调用是没问题的。

#include <iostream>void foo() { std::cout << "HelloWorld"; }
void func()
{return foo();
}int main()
{func();
}

综上所述,略去一些设计过程不表,我们就可以得到这样一个实现。

template<typename Fn>
struct LambdaWrapper {
private:static_assert( std::is_class<Fn>::value, "Only available to lambda" );static_assert( !std::is_empty<Fn>::value, "Only available to lambda with capture" );static const Fn* fntor;template<typename... Args>static typename std::result_of<Fn( Args... )>::type invoking( Args... args )//static std::invoke_result_t<Fn, Args...> invoking( Args... args ){return ( *fntor )( std::forward<Args>( args )... );}public:template<typename... Args>decltype( &invoking<Args...> ) to_fnptr() noexcept{return &invoking<Args...>;}template<typename _FnTp, typename FnTp>friend typename std::enable_if<std::is_class<FnTp>::value && !std::is_empty<FnTp>::value,LambdaWrapper<FnTp>>::typemake_fnptr( _FnTp&& fn ) noexcept;
};
template<typename Fn>
const Fn* LambdaWrapper<Fn>::fntor = nullptr;template<typename _FnTp, typename FnTp = typename std::remove_reference<_FnTp>::type>
typename std::enable_if<std::is_class<FnTp>::value && !std::is_empty<FnTp>::value,LambdaWrapper<FnTp>>::typemake_fnptr( _FnTp&& fn ) noexcept
{static FnTp fntor = std::forward<FnTp>( fn );if ( LambdaWrapper<FnTp>::fntor == nullptr )LambdaWrapper<FnTp>::fntor = std::addressof( fntor );return LambdaWrapper<FnTp>();
}

需要注意的是:函数 invoking 需要获取函数的返回值,而标准库函数 std::result_of 于 C++17 被标记为弃置,并在 C++20 中移除。故对于 C++17 后应当使用 std::invoke_result_t;在 C++17 前使用时请自行切换到被注释掉的行。

方案本身适用于 C++11 及之后的标准。

这个方案有个优点:由于标准中保证了每个 lambda 的类型唯一,所以对于所有相同的 lambda 函数对象,每次调用 make_fnptr 时所指向的内部变量都是相同的。

而且我们还利用了类的访问控制权限,保证了只有 lambda 的创建者才能访问到指向对应的 lambda 对象的函数指针。

但我不保证带引用捕获的 lambda 内部的引用生命周期问题。且这个方案会引入轻微的内存开销。

最后我们就可以这样使用。

#include <bits/stdc++.h>int main()
{int x = 10;// 右值类型的 lambda 没问题auto wrapper = make_fnptr( [&x]( int i ) { return x += i; } );auto lambda = [&x]( int i ) { return x += i; };// 左值类型也没有问题auto wrapper2 = make_fnptr( lambda );// 但是不支持不带捕获的 lambda 函数// 这类函数可以直接转换为函数指针,所以类型转换是没必要的// auto wrapper3 = make_fnptr( []() { std::cout << "Hello World!"; } );// 调用 to_fnptr 方法、并提供函数的参数列表就能获得函数指针auto ptr  = wrapper.to_fnptr<int>();auto ptr2 = wrapper2.to_fnptr<int>();
}

函数的类型参数列表完全由 to_fnptr 的模板参数指定,你给什么它就返回什么。

但是与实际函数的参数列表不匹配的类型参数会导致编译错误。

int main()
{int a = 2, b = 3;double result = 4;auto wrapper = make_fnptr( [&result]( int& a, int& b ) {std::swap( a, b );return ( a + b ) * result;} ); // 参数列表是引用就传入引用auto ptr     = wrapper.to_fnptr<int&, int&>();std::cout << "a = " << a << std::endl << "b = " << b << std::endl;std::cout << "Return = " << ptr( a, b ) << std::endl;std::cout << "a = " << a << std::endl << "b = " << b << std::endl;
}

测试程序见此。

为什么这个包装器只支持带捕获的 lambda 函数对象?

因为其他函数类型都能轻松转换为函数指针。

4.添加一个类型容器

更进一步的,我们可以引入一个用于存储类型的模板;这个模板能够将一组类型在不同模板列表间相互传递。

template<typename...>
struct TypeList {};// 可以这样用
using ArgList = TypeList<int, double, char*>;

虽然这里使用 std::tuple 也可以,但是我们只需要找个地方存放类型,所以最简单的就好。

然后我们直接要求使用者在创建一个 lambda 包装器时,必须显式给出对应 lambda 的函数参数列表;也就是说在 make_fnptr 的模板参数列表额外添加一个类型参数。

过程略,总之我们可以得到如下的实现。

template<typename...>
struct TypeList {};template<typename, typename>
class LambdaWrapper;
template<typename Fn, template<typename...> class List, typename... Args>
class LambdaWrapper<Fn, List<Args...>> {static_assert( std::is_class<Fn>::value, "Only available to lambda" );static_assert( !std::is_empty<Fn>::value, "Only available to lambda with capture" );static_assert( std::is_same<List<Args...>, TypeList<Args...>>::value, "Only accepts TypeList types" );static const Fn* fntor;static typename std::result_of<Fn( Args... )>::type invoking( Args... args )//static std::invoke_result_t<Fn, Args...> invoking( Args... args ){return ( *fntor )( std::forward<Args>( args )... );}template<typename ParamList, typename FnTp>friend typename std::enable_if<std::is_class<typename std::remove_reference<FnTp>::type>::value&& !std::is_empty<typename std::remove_reference<FnTp>::type>::value,decltype( &LambdaWrapper<typename std::remove_reference<FnTp>::type, ParamList>::invoking )>::typemake_fnptr( FnTp&& fn ) noexcept;
};
template<typename Fn, template<typename...> class List, typename... Args>
const Fn* LambdaWrapper<Fn, List<Args...>>::fntor = nullptr;template<typename ParamList = TypeList<>, typename FnTp>
typename std::enable_if<std::is_class<typename std::remove_reference<FnTp>::type>::value&& !std::is_empty<typename std::remove_reference<FnTp>::type>::value,decltype( &LambdaWrapper<typename std::remove_reference<FnTp>::type, ParamList>::invoking )>::typemake_fnptr( FnTp&& fn ) noexcept
{using LambdaType  = typename std::remove_reference<FnTp>::type;using WrapperType = LambdaWrapper<LambdaType, ParamList>;static LambdaType fntor = std::forward<FnTp>( fn );if ( WrapperType::fntor == nullptr )WrapperType::fntor = std::addressof( fntor );return &WrapperType::invoking;
}

现在包装器类就成为了一个纯粹的内部实现,不会暴露任何方法。并且每次调用 make_fnptr 都只会立即产出一个函数指针,而不再会出现中间类对象。


int main()
{int x = 10;std::cout << "x = " << x << std::endl;auto wrapper = make_fnptr<TypeList<int>>( [&x]( int i ) { return x += i; } );auto lambda   = [&x]( int i ) { return x += i; };auto wrapper2 = make_fnptr<TypeList<int>>( lambda );// auto wrapper3 = make_fnptr( []() { std::cout << "Hello World!"; } );std::cout << "x = " << x << std::endl;std::cout << "x = " << x << std::endl;std::cout << "delta = " << wrapper( 5 ) << std::endl;std::cout << "x = " << x << std::endl;std::cout << "delta = " << ( *wrapper2 )( 5 ) << std::endl;std::cout << "x = " << x << std::endl;
}

如果你觉得引入新的类型容器很突兀且不太雅观,那么也可以使用 std::tuple,效果是一样的。

测试程序见此。

如果你愿意为 C++ 的所有函数类型写出一个类型萃取器的话,那么 make_fnptr 的函数参数列表需求其实也可以去除。

5.线程安全改造

在上面的代码中我们可以注意到:由于我们需要使用局部 static 变量的地址初始化另一个 static 变量,所以有且仅有这个初始化过程是线程不安全的。

对于函数内 static 变量而言,它的初始化构造在标准中被明确规定为线程安全,C++11 中有一个术语专门用于描述这种构造技巧:magic static

很显然,类的 static 成员只应该被初始化一次,所以我们可以考虑使用标准库提供的 std::call_once 函数初始化它。也就是像这样改造 make_fnptr 的实现:

  using LambdaType  = typename std::remove_reference<FnTp>::type;using WrapperType = LambdaWrapper<LambdaType, ParamList, InstanceTag>;static std::once_flag seal;static LambdaType fntor = std::forward<FnTp>( fn );std::call_once( seal, []() { WrapperType::fntor = std::addressof( fntor ); } );return &WrapperType::invoking;

lambda 函数本身就是一个仿函数对象;但与 lambda 不同,同种类型的仿函数对象所对应的函数并不相同。例如 std::function(int()) 既可以指 main,也可以指 std::rand

如果希望支持包装一些除了 lambda 之外的仿函数对象,如 std::function 等,那么我们还需要为这个封装器和包装函数提供一个模板参数标识,用于区分同种类型、但不同实例的仿函数对象。

最终的代码见下。

#include <mutex>
#include <type_traits>
#include <utility>template<typename...>
struct TypeList {};template<typename, typename, std::size_t = 0>
class LambdaWrapper;
template<typename Fn, template<typename...> class List, typename... Args, std::size_t Tag>
class LambdaWrapper<Fn, List<Args...>, Tag> {static_assert( std::is_class<Fn>::value, "Only available to lambda" );// 为了支持任意仿函数对象,这里不再约束类的大小static_assert( std::is_same<List<Args...>, TypeList<Args...>>::value,"Only accepts TypeList types" );static const Fn* fntor;public:static typename std::result_of<Fn( Args... )>::type invoking( Args... args )// static std::invoke_result_t<Fn, Args...> invoking( Args... args ){return ( *fntor )( std::forward<Args>( args )... );}template<typename ParamList, std::size_t InstanceTag, typename FnTp>friendtypename std::enable_if<std::is_class<typename std::remove_reference<FnTp>::type>::value,decltype( &LambdaWrapper<typename std::remove_reference<FnTp>::type,ParamList,InstanceTag>::invoking )>::typemake_fnptr( FnTp&& fn ) noexcept;
};
template<typename Fn, template<typename...> class List, typename... Args, std::size_t Tag>
const Fn* LambdaWrapper<Fn, List<Args...>, Tag>::fntor = nullptr;template<typename ParamList = TypeList<>, std::size_t InstanceTag = 0, typename FnTp>
typename std::enable_if<std::is_class<typename std::remove_reference<FnTp>::type>::value,decltype( &LambdaWrapper<typename std::remove_reference<FnTp>::type,ParamList,InstanceTag>::invoking )>::typemake_fnptr( FnTp&& fn ) noexcept
{using LambdaType  = typename std::remove_reference<FnTp>::type;using WrapperType = LambdaWrapper<LambdaType, ParamList, InstanceTag>;static std::once_flag seal;static LambdaType fntor = std::forward<FnTp>( fn );std::call_once( seal, []() { WrapperType::fntor = std::addressof( fntor ); } );return &WrapperType::invoking;
}

对于 lambda 和仿函数对象,我们可以有如下使用方法。

#include <iostream>struct Functor {int operator()( int x, int y ) const noexcept { return x + y; }
};int main()
{int x = 10, y = 20;auto wrapper = make_fnptr<TypeList<int>>( [&x]( int i ) { return x += i; } );std::cout << "x = " << x << std::endl;std::cout << "delta = " << wrapper( 5 ) << std::endl;std::cout << "x = " << x << std::endl;Functor fntor1, fntor2;auto wrapper2 = make_fnptr<TypeList<int, int>, 0>( fntor1 );auto wrapper3 = make_fnptr<TypeList<int, int>, 1>( fntor2 );std::cout << "sum = " << wrapper2( x, y ) << std::endl;std::cout << "sum = " << wrapper3( 114, 514 ) << std::endl;
}

注意 make_fnptr 函数中构造 funtor 的方式,如果传递一个左值形式的不可拷贝仿函数对象,会导致编译错误;右值形式的不可移动对象同理。

代码不对仿函数对象的私有引用提供任何生命周期保证。

测试程序见此。

6.Reference

感谢 bilibili@Ayano_Aishi 在视频 BV1Hm421j7qc 下方的评论区中提供了本文代码的原始版本以及本文的灵感来源。

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

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

相关文章

boost之property

简介 property在boost.graph中有使用&#xff0c;用于表示点属性或者边属性 结构 #mermaid-svg-56YI0wFLPH0wixrJ {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-56YI0wFLPH0wixrJ .error-icon{fill:#552222;}#me…

Oracle 19c PDB克隆后出现Warning: PDB altered with errors受限模式处理

在进行一次19c PDB克隆过程中&#xff0c;发现克隆结束&#xff0c;在打开后出现了报错&#xff0c;PDB变成受限模式&#xff0c;以下是分析处理过程 09:25:48 SQL> alter pluggable database test1113 open instancesall; Warning: PDB altered with errors. Elapsed: 0…

AndroidStudio-Activity的生命周期

一、Avtivity的启动和结束 从当前页面跳到新页面&#xff0c;跳转代码如下&#xff1a; startActivity(new Intent(源页面.this&#xff0c;目标页面.class))&#xff1b; 从当前页面回到上一个页面&#xff0c;相当于关闭当前页面&#xff0c;返回代码如下&#xff1a; finis…

ubuntu20.04 解决Pycharm没有写入权限,无法通过检查更新更新的问题

ubuntu20.04 解决Pycharm没有写入权限&#xff0c;无法通过检查更新更新的问题 您提供的截图显示了一个关于PyCharm更新的问题&#xff0c;其中提到了&#xff1a;“PyCharm 没有 /opt/pycharm-community-2024.1.2 的写入权限&#xff0c;请通过特权用户运行以更新。” 这表明…

云原生之运维监控实践-使用Telegraf、Prometheus与Grafana实现对InfluxDB服务的监测

背景 如果你要为应用程序构建规范或用户故事&#xff0c;那么务必先把应用程序每个组件的监控指标考虑进来&#xff0c;千万不要等到项目结束或部署之前再做这件事情。——《Prometheus监控实战》 去年写了一篇在Docker环境下部署若依微服务ruoyi-cloud项目的文章&#xff0c;当…

WinDefender Weaker

PPL Windows Vista / Server 2008引入 了受保护进程的概念&#xff0c;其目的不是保护您的数据或凭据。其最初目标是保护媒体内容并符合DRM &#xff08;数字版权管理&#xff09;要求。Microsoft开发了此机制&#xff0c;以便您的媒体播放器可以读取例如蓝光&#xff0c;同时…

Python 编程入门指南(二)

1. 条件语句 条件语句用于根据条件的真假来控制代码的执行流。在Python中,可以使用if、elif和else关键字来实现条件判断。 例如: x = 10 y = 20 if y > x:print("y 大于 x") elif y == x:

计算机视觉 1-8章 (硕士)

文章目录 零、前言1.先行课程&#xff1a;python、深度学习、数字图像处理2.查文献3.环境安装 第一章&#xff1a;概论1.计算机视觉的概念2.机器学习 第二章&#xff1a;图像处理相关基础1.图像的概念2.图像处理3.滤波器4.卷积神经网络CNN5.图像的多层表示&#xff1a;图像金字…

实习冲刺练习 第二十三天

每日一题 回文链表. - 力扣&#xff08;LeetCode&#xff09; class Solution { public:bool isPalindrome(ListNode* head) {if(headnullptr) return false;vector<int> v;while(head!nullptr){//将链表的值存入数组中v.push_back(head->val);headhead->next;}in…

《C++ 实现生成多个弹窗程序》

《C 实现生成多个弹窗程序》 在 C 编程中&#xff0c;我们可以利用特定的系统函数来创建弹窗&#xff0c;实现向用户展示信息等功能。当需要生成多个弹窗时&#xff0c;我们可以通过循环结构等方式来达成这一目的。 一、所需头文件及函数介绍 在 Windows 操作系统环境下&#…

报错 No available slot found for the embedding model

报错内容 Server error: 503 - [address0.0.0.0:12781, pid304366] No available slot found for the embedding model. We recommend to launch the embedding model first, and then launch the LLM models. 目前GPU占用情况如下 解决办法: 关闭大模型, 先把 embedding mode…

RabbitMQ介绍和快速上手案例

文章目录 1.引入1.1同步和异步1.2消息队列的作用1.3rabbitMQ介绍 2.安装教程2.1更新软件包2.2安装erlang2.3查看这个erlang版本2.4安装rabbitMQ2.5安装管理页面2.6浏览器测试2.7添加管理员用户 3.rabbitMQ工作流程4.核心概念介绍4.1信道和连接4.2virtual host4.3quene队列 5.We…

《PCA 原理推导》18-5线性变换生成的随机变量y_i和y_j的协方差 公式解析

本文是将文章《PCA 原理推导》中的公式单独拿出来做一个详细的解析&#xff0c;便于初学者更好的理解。 公式 18 - 5 18\text{-}5 18-5 的内容如下&#xff1a; cov ( y i , y j ) a i T Σ a j , i , j 1 , 2 , … , m \text{cov}(y_i, y_j) a_i^T \Sigma a_j, \quad i, j…

数据结构(初阶4)---循环队列详解

循环队列 1.循环队列的结构  1).逻辑模式 2.实现接口  1).初始化  2).判断空和满  3).增加  4).删除  5).找头  6).找尾 3.循环队列的特点 1.循环队列的结构 1).逻辑模式 与队列是大同小异的&#xff0c; 其中还是有一个指向队列头的head指针&#xff0c; 也有一个指向尾…

C++知识点总结(57):STL综合

STL综合 一、数据结构1. 队列2. 映射 二、队列例题1. 约瑟夫环&#xff08;数据加强&#xff09;2. 打印队列3. 小组队列4. 日志统计 2.0 三、映射真题1. 眼红的 Medusa2. 美食评委 一、数据结构 1. 队列 功能代码定义queue<tp>q入队.push(x)出队.pop()队头.front()队尾…

java中volatile 类型变量提供什么保证?能使得一个非原子操作变成原子操作吗?

大家好&#xff0c;我是锋哥。今天分享关于【java中volatile 类型变量提供什么保证&#xff1f;能使得一个非原子操作变成原子操作吗&#xff1f;】面试题。希望对大家有帮助&#xff1b; java中volatile 类型变量提供什么保证&#xff1f;能使得一个非原子操作变成原子操作吗&…

Python - 初识Python;Python解释器下载安装;Python IDE(一)

一、初识Python Python 是一种高级编程语言&#xff0c;Python是一种面向对象的解释型计算机程序设计语言&#xff0c;Python由荷兰国家数学与计算机科学研究中心的吉多范罗苏姆&#xff08;&#xff09;Guido van Rossum吉多范罗苏姆&#xff08;&#xff09;于1989 年底发明…

flink StreamGraph 构造flink任务

文章目录 背景主要步骤代码 背景 通常使用flink 提供的高级算子来编写flink 任务&#xff0c;对底层不是很了解&#xff0c;尤其是如何生成作业图的细节 下面通过构造一个有向无环图&#xff0c;来实际看一下 主要步骤 1.增加source 2.增加operator 3. 增加一条边&#xff0…

AR眼镜方案_AR智能眼镜阵列/衍射光波导显示方案

在当今AR智能眼镜的发展中&#xff0c;显示和光学组件成为了技术攻坚的主要领域。由于这些组件的高制造难度和成本&#xff0c;其光学显示模块在整个设备的成本中约占40%。 采用光波导技术的AR眼镜显示方案&#xff0c;核心结构通常由光机、波导和耦合器组成。光机内的微型显示…

六:从五种架构风格推导出HTTP的REST架构

在分布式系统中,架构风格(Architectural Style)决定了系统组件如何交互、通信、存储和管理数据。每种架构风格都有其独特的特性和适用场景。本文将从五种典型的架构风格出发,逐步探讨它们如何影响了REST(Representational State Transfer,表述性状态转移)架构风格的设计…