C++:std::function的libc++实现

std::function是个有点神奇的模板,无论是普通函数、函数对象、lambda表达式还是std::bind的返回值(以上统称为可调用对象(Callable)),无论可调用对象的实际类型是什么,无论是有状态的还是无状态的,只要它们有相同参数类型和返回值类型,就可以使用同一类型的std::function进行存储和调用。这种特性被称作类型擦除(Type erasure),它允许我们在不知道对象实际类型的情况下对对象进行存储和操作。

在本文中,我将以std::functionlibc++实现(14.0版本)为例,分析std::function类型擦除的实现原理,以及实现一个精简版的std::functionMyFunction

std::function如何实现类型擦除?

在不知道对象实际类型的情况下操作对象,有一种常规的手段可以实现这个功能,那就是多态,libc++版的std::function正是基于虚函数实现的。具体是如何实现的呢?我们可以从考察std::function在被调用时发生了什么作为这个问题的切入点。

对于以下代码:

#include <functional>
#include <iostream>
int main() {
  std::function<void()> f = []() {  //
    std::cout << "Hello, world!" << std::endl;
  };
  f();
  return 0;
}

std::cout一行打断点,运行,得到以下堆栈:

#0  main::$_0::operator() (this=0x7fffffffdb18) at /mnt/d/code/function_test/call.cpp:6
#1  0x0000555555557745 in std::__1::__invoke<main::$_0&> (__f=...) at /usr/lib/llvm-14/bin/../include/c++/v1/type_traits:3640
#2  0x00005555555576fd in std::__1::__invoke_void_return_wrapper<void, true>::__call<main::$_0&> (__args=...) at /usr/lib/llvm-14/bin/../include/c++/v1/__functional/invoke.h:61
#3  0x00005555555576cd in std::__1::__function::__alloc_func<main::$_0, std::__1::allocator<main::$_0>, void ()>::operator()() (this=0x7fffffffdb18) at /usr/lib/llvm-14/bin/../include/c++/v1/__functional/function.h:180
#4  0x0000555555556839 in std::__1::__function::__func<main::$_0, std::__1::allocator<main::$_0>, void ()>::operator()() (this=0x7fffffffdb10) at /usr/lib/llvm-14/bin/../include/c++/v1/__functional/function.h:354
#5  0x0000555555558622 in std::__1::__function::__value_func<void ()>::operator()() const (this=0x7fffffffdb10) at /usr/lib/llvm-14/bin/../include/c++/v1/__functional/function.h:507
#6  0x00005555555577d5 in std::__1::function<void ()>::operator()() const (this=0x7fffffffdb10) at /usr/lib/llvm-14/bin/../include/c++/v1/__functional/function.h:1184
#7  0x00005555555562e5 in main () at /mnt/d/code/function_test/call.cpp:8

不考虑lambda本身,以及invoke相关的类,std::function实现相关的类有以下几个:

  1. std::__1::function<void ()>
  2. std::__1::__function::__value_func<void ()>
  3. std::__1::__function::__func<main::$_0, std::__1::allocator<main::$_0>, void ()>
  4. std::__1::__function::__alloc_func<main::$_0, std::__1::allocator<main::$_0>, void ()>

lambda的类型被定义为了main::$_0,可以看出来,function__function::__value_func两个模板类不依赖lambda实际类型,__function::__func__function::__alloc_func对lambda类型有依赖。

std::function

std::function看起,被声明为拥有一个模板参数_Fp。我们使用的是它的特化版本,具有两个模板参数,返回值类型_Rp和参数列表类型_ArgTypes(接下来几个类也都是特化出来的,不再赘述)。它有一个__function::__value_func<_Rp(_ArgTypes...)>类型的成员__f_

template<class _Fp> class function;

template<class _Rp, class ..._ArgTypes>
class function<_Rp(_ArgTypes...)>
{
    typedef __function::__value_func<_Rp(_ArgTypes...)> __func;
    __func __f_;
    ...
};
...
template <class _Rp, class... _ArgTypes>
template <class _Fp, class>
function<_Rp(_ArgTypes...)>::function(_Fp __f) : __f_(_VSTD::move(__f)) {}

std::functionoperator()被调用时,它只是地把调用转发给__f_

template<class _Rp, class ..._ArgTypes>
_Rp
function<_Rp(_ArgTypes...)>::operator()(_ArgTypes... __arg) const
{
    return __f_(_VSTD::forward<_ArgTypes>(__arg)...);
}

__function::__value_func

看看__function::__value_func具体是什么类型:

// __value_func creates a value-type from a __func.
template <class _Fp> class __value_func;

template <class _Rp, class... _ArgTypes> class __value_func<_Rp(_ArgTypes...)>
{
    typename aligned_storage<3 * sizeof(void*)>::type __buf_;

    typedef __base<_Rp(_ArgTypes...)> __func;
    __func* __f_;
    ...
};

它的模板参数和std::function一致,有两个成员,一个成员是有3个指针大小的__buf_,另一个成员是__function::__base<_Rp(_ArgTypes...)>*类型的__f_

__function::__value_func的构造函数相对复杂一些,主要是为了做一个优化:当__f_指向的对象的大小小于等于__buf_的大小,也就是3个指针时,__f_会被构造在__buf_上,这样可以减少堆上内存的分配:

template <class _Fp, class _Alloc>
__value_func(_Fp&& __f, const _Alloc& __a)
    : __f_(nullptr)
{
    typedef allocator_traits<_Alloc> __alloc_traits;
    typedef __function::__func<_Fp, _Alloc, _Rp(_ArgTypes...)> _Fun;
    typedef typename __rebind_alloc_helper<__alloc_traits, _Fun>::type
        _FunAlloc;

    if (__function::__not_null(__f))
    {
        _FunAlloc __af(__a);
        if (sizeof(_Fun) <= sizeof(__buf_) &&
            is_nothrow_copy_constructible<_Fp>::value &&
            is_nothrow_copy_constructible<_FunAlloc>::value)
        {
            __f_ =
                ::new ((void*)&__buf_) _Fun(_VSTD::move(__f), _Alloc(__af));
        }
        else
        {
            typedef __allocator_destructor<_FunAlloc> _Dp;
            unique_ptr<__func, _Dp> __hold(__af.allocate(1), _Dp(__af, 1));
            ::new ((void*)__hold.get()) _Fun(_VSTD::move(__f), _Alloc(__a));
            __f_ = __hold.release();
        }
    }
}

需要注意到的一个细节是:__f_在模板类定义中的类型是__function::__base,而此处new出来的对象类型是__function::__func,不难猜到,__function::__func继承了__function::__base

__function::__value_funcoperator()被调用时,它也只是在做完合法性检查后把调用转发给了*__f_

_Rp operator()(_ArgTypes&&... __args) const
{
    if (__f_ == nullptr)
        __throw_bad_function_call();
    return (*__f_)(_VSTD::forward<_ArgTypes>(__args)...);
}

__function::__base

下面是__function::__base,它是一个抽象模板类,模板参数和std::function一致,不包含可调用对象的具体类型:

template<class _Fp> class __base;

template<class _Rp, class ..._ArgTypes>
class __base<_Rp(_ArgTypes...)>
{
    __base(const __base&);
    __base& operator=(const __base&);
public:
    _LIBCPP_INLINE_VISIBILITY __base() {}
    _LIBCPP_INLINE_VISIBILITY virtual ~__base() {}
    virtual __base* __clone() const = 0;
    virtual void __clone(__base*) const = 0;
    virtual void destroy() _NOEXCEPT = 0;
    virtual void destroy_deallocate() _NOEXCEPT = 0;
    virtual _Rp operator()(_ArgTypes&& ...) = 0;
#ifndef _LIBCPP_NO_RTTI
    virtual const void* target(const type_info&) const _NOEXCEPT = 0;
    virtual const std::type_info& target_type() const _NOEXCEPT = 0;
#endif // _LIBCPP_NO_RTTI
};

__function::__func

然后是__function::__func,它继承了__function::__base,并且其模板参数含有可调用对象的类型_Fp,这正是实现类型擦除的关键:类型_Fp被隐藏了在了__function::__base这个抽象类后面。__function::__func含有一个类型为__function::__alloc_func的成员__f_

// __func implements __base for a given functor type.
template<class _FD, class _Alloc, class _FB> class __func;

template<class _Fp, class _Alloc, class _Rp, class ..._ArgTypes>
class __func<_Fp, _Alloc, _Rp(_ArgTypes...)>
    : public  __base<_Rp(_ArgTypes...)>
{
    __alloc_func<_Fp, _Alloc, _Rp(_ArgTypes...)> __f_;
public:
    explicit __func(_Fp&& __f)
        : __f_(_VSTD::move(__f)) {}
    ...
};

__function::__funcoperator()依然只是转发调用:

template<class _Fp, class _Alloc, class _Rp, class ..._ArgTypes>
_Rp
__func<_Fp, _Alloc, _Rp(_ArgTypes...)>::operator()(_ArgTypes&& ... __arg)
{
    return __f_(_VSTD::forward<_ArgTypes>(__arg)...);
}

__function::__alloc_func

然后是最后一个类__function::__alloc_func,它有一个pair类型的成员__f_std::function构造时传入的可调用对象最终会存储在__f_中:

// __alloc_func holds a functor and an allocator.
template <class _Fp, class _Ap, class _FB> class __alloc_func;

template <class _Fp, class _Ap, class _Rp, class... _ArgTypes>
class __alloc_func<_Fp, _Ap, _Rp(_ArgTypes...)>
{
    __compressed_pair<_Fp, _Ap> __f_;

  public:
    ...
    explicit __alloc_func(_Target&& __f)
        : __f_(piecewise_construct, _VSTD::forward_as_tuple(_VSTD::move(__f)),
               _VSTD::forward_as_tuple())
    {
    }
    ...
};

__function::__alloc_funcoperator()方法中,调用转发给了__invoke_void_return_wrapper::__call,后面的流程就和std::function的实现无关了。

_Rp operator()(_ArgTypes&&... __arg)
{
    typedef __invoke_void_return_wrapper<_Rp> _Invoker;
    return _Invoker::__call(__f_.first(),
                            _VSTD::forward<_ArgTypes>(__arg)...);
}

最终我们发现,“神奇”的类型擦除还是通过“朴素”的多态来实现的,之所以显得神奇是因为多态被隐藏了起来,没有暴露给用户。

std::function对构造参数的校验

仔细观察一下std::function的构造函数:

template <class _Rp, class... _ArgTypes>
template <class _Fp, class>
function<_Rp(_ArgTypes...)>::function(_Fp __f) : __f_(_VSTD::move(__f)) {}

构造函数对参数__f似乎并没有施加任何约束,如何真是那样,那我们在使用一个不恰当的_Fp类型构造std::function时,很可能会得到可读性极差的编译错误信息,因为std::function类本身对_Fp没有施加约束,那么实例化std::function时也就不太可能出现错误了,很有可能到了实例化__function::__alloc_func时编译错误才会报告出来,这是一个内部类,一般用户看到了关于它的实例化失败的错误信息大概会感到摸不着头脑。

但实际情况并不是这样的,假设你这样定义一个std::function对象:

std::function<void()> f(1);

你会得到一个比较清晰的编译错误信息:

/mnt/d/code/function_test/myfunction.cpp:107:27: error: no matching constructor for initialization of 'std::function<void ()>'
    std::function<void()> f(1);
...
/usr/lib/llvm-14/bin/../include/c++/v1/__functional/function.h:998:5: note: candidate template ignored: requirement '__callable<int &, false>::value' was not satisfied [with _Fp = int]
    function(_Fp);
...

这是怎么做到的呢?答案藏在构造函数声明的第二个模板参数class = _EnableIfLValueCallable<_Fp>

template<class _Fp, class = _EnableIfLValueCallable<_Fp>>
function(_Fp);

此处使用了SFINAE技术,我们看看_EnableIfLValueCallable具体是怎么实现的:

template <class _Fp, bool = _And<
    _IsNotSame<__uncvref_t<_Fp>, function>,
    __invokable<_Fp, _ArgTypes...>
>::value>
struct __callable;
template <class _Fp>
    struct __callable<_Fp, true>
    {
        static const bool value = is_void<_Rp>::value ||
            __is_core_convertible<typename __invoke_of<_Fp, _ArgTypes...>::type,
                                    _Rp>::value;
    };
template <class _Fp>
    struct __callable<_Fp, false>
    {
        static const bool value = false;
    };

template <class _Fp>
using _EnableIfLValueCallable = typename enable_if<__callable<_Fp&>::value>::type;

_EnableIfLValueCallable的实现依赖于__callable__callable是一个模板类,拥有两个模板参数,第一个模板参数_Fp是可调用对象的类型,第二个模板参数是bool类型的,当_IsNotSame<__uncvref_t<_Fp>, function>__invokable<_Fp, _ArgTypes...>这两个条件同时满足时,该模板参数为true,否则为false。

_IsNotSame<__uncvref_t<_Fp>, function>,顾名思义,是用来判断两个模板参数是否为同一类型的,这个条件似乎是为了避免歧义:当我们用另一个std::function构造std::function时,应该匹配到拷贝构造函数,而不是这个。

__invokable<_Fp, _ArgTypes...>则是用来判断_Fp是否接受传入_ArgTypes参数调用。

__callable第二个模板参数为false的特化中,将value直接定义为false。而模板参数为true的特化中,还添加了新的判断条件,用来校验可调用对象返回值的可转换性。

第一个条件为is_void<_Rp>::value,用来判断_Rpvoid类型。这意味着,即使可调用对象实际上有返回类型,但是std::function被定义为返回void,那么编译也是可以通过的。

第二个条件是__is_core_convertible<typename __invoke_of<_Fp, _ArgTypes...>::type, _Rp>::value,用来判断_Fp被调用后返回值可转换为_Rp

综上,_Fp要满足以下条件,std::function的构造函数才能正常实例化:

_Fp不是std::function && _Fp可以以_ArgTypes为参数调用 && (_Rpvoid || _Fp返回值类型可转换为_Rp)

这保证了当以不恰当的可调用对象构造std::function时,能够尽可能提前触发编译错误,提升编译错误信息的可读性。

MyFunction的实现

下面我们下面模仿libc++,实现一个“青春版”的std::functionMyFunction,它忽略掉了大部分细节,只实现了构造和调用部分的代码。

#include <functional>
#include <iostream>
#include <utility>

template <typename Func>
class FunctionBase;

template <typename Ret, typename... Args>
class FunctionBase<Ret(Args...)> {
 public:
  virtual Ret operator()(Args&&... args) = 0;
};

template <typename Callable, typename Func>
class FunctionImpl;

template <typename Callable, typename Ret, typename... Args>
class FunctionImpl<Callable, Ret(Args...)> : public FunctionBase<Ret(Args...)> {
  Callable c_;

 public:
  FunctionImpl(Callable&& c) : c_(std::move(c)) {}
  Ret operator()(Args&&... args) override {
    return std::invoke(c_, std::forward<Args>(args)...);
  }
};

template <typename Func>
class MyFunction;

template <typename Ret, typename... Args>
class MyFunction<Ret(Args...)> {
  FunctionBase<Ret(Args...)>* f_ = nullptr;

 public:
  template <typename Callable>
  MyFunction(Callable c) {
    f_ = new FunctionImpl<Callable, Ret(Args...)>(std::move(c));
  }
  Ret operator()(Args&&... args) {
    if (f_ == nullptr) {
      throw std::bad_function_call();
    }
    return (*f_)(std::forward<Args>(args)...);
  }
};

void normalFunction() { std::cout << "I'm a normal function" << std::endl; }

struct FunctionObject {
  void operator()() { std::cout << "I'm a function object" << std::endl; }
};

int main() {
  MyFunction<void()> f0 = []() { std::cout << "I'm a lambda" << std::endl; };
  f0();

  MyFunction<void()> f1 = normalFunction;
  f1();

  MyFunction<void()> f2 = FunctionObject();
  f2();

  return 0;
}

结语

在没有std::function可用的年代或者场合,我们一般会选择使用函数指针来实现类似std::function的功能。在使用C实现的Linux内核代码中,我们仍可以看到大量的函数指针的存在,主要是用来实现回调函数。

相较函数指针,std::function最明显的优势在于可以方便地存储带状态的函数,而函数指针只能以比较丑陋的方式来实现这个特性。

其次是灵活性,std::function给客户代码施加的约束较小,我们可以使用任意形式的可调用对象:普通函数,lambda表达式,函数对象等,函数指针就没有这种灵活性了。

不过由于虚函数的存在,std::function多了一点性能开销,但这点开销对大多数常规应用来说都是微不足道的。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

在线教育项目(一):如何防止一个账号多个地方登陆

使用jwt做验证&#xff0c;使用账号作为redis中的key,登录的时候生成token放到redis中&#xff0c;每次申请资源的时候去看token 有没有变&#xff0c;因为token每次登录都会去覆盖&#xff0c;只要第二次登录token就不一样了

在我们的大数据平台(XSailbaot)上进行企业级数据建模的思路

1. 背景 笔者所在的公司是差不多二十年前搞CIM&#xff08;公共信息模型的&#xff09;起家的。当时公司的前辈搞了基于CIS协议的模型服务器、数据服务器、模式编辑器等&#xff0c;形成了一套基于公共信息模型建模的平台系统。其中可视化建模&#xff0c;建好了模式类以后&am…

SA 注册流程

目录 1. UE开机后按照3GPP TS 38.104定义的Synchronization Raster搜索特定频点 2.UE尝试检测PSS/SSS&#xff0c;取得下行时钟同步&#xff0c;并获取小区的PCI&#xff1b;如果失败则转步骤1搜索下一个频点&#xff1b;否则继续后续步骤&#xff1b; 3.解析Mib&#xff0c;…

设计NOR Flash(SPI接口)的Flashloader(MCU: stm32f4)

目录 概述 1 软硬件 1.1 软硬件信息表 1.2 NOR Flash芯片&#xff08;W25Q64BVSSI&#xff09; 1.2.1 W25Q64BVSSI芯片介绍 1.2.2 NOR Flash接口 1.3 MCU与NOR Flash接口 2 SPI Flash功能实现 2.1 软件框架结构 2.2 代码实现 2.2.1 Dev_Inf文件 2.2.2 W25QXX驱动程…

区间动态规划——最长回文子串(C++)

难得心静。 ——2024年6月30日 什么是区间动态规划&#xff1f; 区间动态规划通常以连续区间的求解作为子问题&#xff0c;例如区间 [i, j] 上的最优解用dp[i][j]表示。先在小区间上进行动态规划得到子问题的最优解&#xff0c;再利用小区间的最优解合并产生大区间的最优解。 …

娱乐圈发生震动,AI大模型技术已经取代了SNH48的小偶像?

自2023年以来&#xff0c;全球都被包裹在AI的惊天大潮之中&#xff0c;所有行业都在主动或被动地迎接改变。目前&#xff0c;各行业已经有大量公司正在把AI作为自身发展的最佳路径。其中&#xff0c;娱乐行业作为最被人们熟知的行业也在面对AI的发展时&#xff0c;发生着巨大变…

视频共享融合赋能平台LntonCVS统一视频接入平台数字化升级医疗体系

医疗健康事关国计民生&#xff0c;然而&#xff0c;当前我国医疗水平的地区发展不平衡、医疗资源分布不均和医疗信息系统老化等问题&#xff0c;制约了整体服务能力和水平的提升。视频融合云平台作为推动数字医疗的关键工具&#xff0c;在医疗领域的广泛应用和普及&#xff0c;…

统计信号处理基础 习题解答11-1

题目 观测到的数据具有PDF 在μ给定的条件下&#xff0c;是相互独立的。均值具有先验PDF&#xff1a; 求μ的 MMSE 和 MAP 估计量。另外&#xff0c;当和时将发生什么情况? 解答 和两者都是独立高斯分布&#xff0c;与例题10.1一致&#xff0c;直接套用&#xff08;10.11&am…

RedisAtomicInteger并发案例

&#x1f370; 个人主页:__Aurora__ &#x1f35e;文章有不合理的地方请各位大佬指正。 &#x1f349;文章不定期持续更新&#xff0c;如果我的文章对你有帮助➡️ 关注&#x1f64f;&#x1f3fb; 点赞&#x1f44d; 收藏⭐️ RedisAtomicInteger 提供了对整数的原子性操作&a…

《昇思25天学习打卡营第14天 | 昇思MindSpore基于MindNLP+MusicGen生成自己的个性化音乐》

14天 本节学了基于MindNLPMusicGen生成自己的个性化音乐。 MusicGen是来自Meta AI的Jade Copet等人提出的基于单个语言模型的音乐生成模型&#xff0c;能够根据文本描述或音频提示生成高质量的音乐样本。 MusicGen模型基于Transformer结构&#xff0c;可以分解为三个不同的阶段…

C++: 如何用C语言实现C++的虚函数机制?

前言 在 googletest的源码中&#xff0c;看到gtest-matchers.h 中实现的MatcherBase 类自定义了一个 VTable&#xff0c;这种设计实现了一种类似于C虚函数的机制。C中的虚函数机制实质上就是通过这种方式实现的&#xff0c;本文用c语言自定义虚函数表VTable实现了一下virtual的…

等保主机测评防骗指南(资产调研)

你是否测评时常被运维给忽悠&#xff1f;是否觉得以下的对话耳熟&#xff1f; 你&#xff1a;您好&#xff0c;请问你们的主机资产有哪些&#xff0c;包括服务器、数据库、中间件、应用系统等。 甲&#xff1a;我们资产就这两台服务器&#xff0c;数据库什么的都这上面&#…

OpenGL3.3_C++_Windows(25)

阴影失真:阴影的不真实感 条纹样式&#xff1a; 首先理解采样原理&#xff1a;同光的视角下&#xff0c;渲染一张深度图&#xff0c;每个像素&#xff0c;存储同一射线下的深度值&#xff08;不断更新深度缓冲的结果&#xff09;&#xff0c;即最近片段的深度。接着&#xff0…

hadoop词频统计

1 Hadoop 安装与伪分布的搭建 2 Hadoop词频统计 此文章基于搭建好hadoop之后做的词频统计实验&#xff0c;以上是链接为搭建hadoop的教程 目录 1 HDFS 文件系统常用命令 2 词频统计实验准备工作 2.1 启动hadoop 关闭防火墙 2.2 查看图形化界面 2.3 文件上传 3 词频统计…

isspace()方法——判断字符串是否只由空格组成

自学python如何成为大佬(目录): https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 语法参考 isspace()方法用于判断字符串是否只由空格组成。isspace()方法的语法格式如下&#xff1a; str.isspace() 如果字符串中只包含空格&…

【Unity设计模式】✨使用 MVC 和 MVP 编程模式

前言 最近在学习Unity游戏设计模式&#xff0c;看到两本比较适合入门的书&#xff0c;一本是unity官方的 《Level up your programming with game programming patterns》 ,另一本是 《游戏编程模式》 这两本书介绍了大部分会使用到的设计模式&#xff0c;因此很值得学习 本…

【算法】5分钟了解如何使用PCA主成份分析

本文来自《老饼讲解-BP神经网络》https://www.bbbdata.com/ 目录 一、什么是PCA1.1.PCA的思想1.2.PCA的数学表示 二、什么是PCA的主成份与方差2.1.主成份的方差2.2.主成份的命名 三、如何使用PCA3.1.主成份的代码实现 主成份分析全称为PCA Principle Component Analysis ,它的主…

Linux虚拟串口设置

VSPD虚拟串口软件安装及使用 一、软件安装 1、Configure Virtual Serial Port Driver(VSPD) 1.1 首先下载 Configure Virtual Serial Port Driver(VSPD) 软件 链接&#xff1a;https://pan.baidu.com/s/11aGc2aHGUew5QZ0XhaWXJw 提取码&#xff1a;rmd7 1.2 安装时注意将…

计算机基础之汇编语言学习笔记

学习来源&#xff1a;b站各种学习资料 前置知识&#xff1a;计算机组成原理等知识 学习参考的资源 汇编语言编程的速成指南[上]~从零开始的期末抢救计划 &#xff08;8086汇编&#xff09;_哔哩哔哩_bilibili 链接: https://pan.baidu.com/s/1tg_ZW7VD3TS_s1v_EjS89w?pwdak6…

2029年AI服务器出货量将突破450万台,AI推理服务器即将爆发式增长

在2020年&#xff0c;新冠疫情与远程办公模式的兴起推动了所有类型服务器的出货量达到峰值&#xff0c;随后几年里&#xff0c;除了AI服务器之外的所有类别都回归到了正常水平。 根据Omdia的研究数据&#xff0c;AI服务器的出货量在2020年急剧上升&#xff0c;并且至今未显示出…