关于有害的过度使用 std::move

翻译:2023 11 月 24 日On harmful overuse of std::move

cppreference std::move

论 std::move 的有害过度使用 - The Old New Thing

C++ 的 std::move 函数将其参数转换为右值引用,这使得其内容可以被另一个操作“消费”(移动)。但是,在你为这个新表达能力兴奋不已时,请注意不要过度使用它。

std::string get_name(int id) {std::string name = std::to_string(id);/* 假设这里进行了其他计算 */return std::move(name); // 过度使用 move 的错误示范
}

你可能认为你在通过说“嘿,你看,我在之后不会使用我的局部变量 name 了,所以你可以直接把字符串移动到返回值里”来给编译器一些帮助。不幸的是,你的“帮助”实际上造成了伤害。

添加 std::move 会导致返回语句不能满足复制省略(Copy Elision) (通常称为命名返回值优化,NRVO)的条件:返回的东西必须是与函数返回值类型相同的局部变量的名称。添加的 std::move 阻止了 NRVO,返回值是通过移动构造函数从 name 变量构造的。

std::string get_name(int id) {std::string name = std::to_string(id);/* 假设这里进行了其他计算 */return name; // 正确方式:允许 NRVO
}

这次,我们直接返回 name,编译器现在可以省略拷贝,直接将 name 变量放入返回值槽中,无需拷贝。(编译器被允许但不强制进行此优化;但在实践中,如果所有代码路径都返回同一个局部变量,所有编译器都会这样做。)

过度热衷使用 std::move 的另一半问题发生在接收端。

extern void report_name(std::string name);void sample1() {std::string name = std::move(get_name()); // 过度使用 move 的错误示范
}void sample2() {report_name(std::move(get_name())); // 过度使用 move 的错误示范
}

在这两个示例函数中,我们获取 get_name() 的返回值,并显式地 std::move 它到一个新的局部变量或函数参数中。这是另一个试图帮忙却最终帮倒忙的例子。

从一个匹配类型的值构造一个值(无论是局部变量还是函数参数)会被省略:匹配的值被直接存储到局部变量或参数中,无需拷贝。但添加 std::move 阻止了此优化发生,该值将通过移动构造。

extern void report_name(std::string name);void sample1() {std::string name = get_name(); // 正确方式:允许初始化省略
}void sample2() {report_name(get_name()); // 正确方式:允许参数初始化省略
}

特别“精彩”的是当你把两个错误结合在一起时。那样的话,你把一个本来完全没有拷贝或移动操作的序列,变成了一个创建了两个额外的临时对象、两次额外的移动操作和两次额外的析构操作的序列。

#include <memory>
struct S {S();S(S const&);S(S &&);~S();
};
extern void consume(S s);// 错误版本
S __declspec(noinline) f1() {S s;return std::move(s); // 错误 1:阻止 NRVO
}void g1() {consume(std::move(f1())); // 错误 2:阻止初始化省略
}

(展示MSVC 为错误版本 f1/g1 和正确版本 f2/g2 生成的汇编代码,清晰地证明了错误版本进行了额外的移动构造和临时对象操作,而正确版本利用 NRVO 和初始化省略实现了零拷贝/移动。)

以下是 msvc 的编译器输出:

; on entry, rcx says where to put the return value在入口处,rcx指出将返回值放在何处。
f1:mov     qword ptr [rsp+8], rcxpush    rbxsub     rsp, 48mov     rbx, rcx; construct local variable s on stack在堆栈上构造局部变量slea     rcx, qword ptr [rsp+64]call    S::S(); copy local variable to return value复制局部变量到返回值lea     rdx, qword ptr [rsp+64]mov     rcx, rbxcall    S::S(S &&); destruct the local variable s析构局部变量 slea     rcx, qword ptr [rsp+64]call    S::~S(); return the result返回结果mov     rax, rbxadd     rsp, 48pop     rbxretg1:sub     rsp, 40; call f1 and store into temporary variable调用f1并存储到临时变量中lea     rcx, qword ptr [rsp+56]call    f1(); copy temporary to outbound parameter复制临时到出站参数mov     rdx, raxlea     rcx, qword ptr [rsp+48]call    S::S(S &&); call consume with the outbound parameter使用出站参数调用消费mov     rcx, raxcall    consume(S); clean up the temporary清理临时的lea     rcx, qword ptr [rsp+56]call    S::~S(); returnadd     rsp, 40ret

请注意,调用 g1 会导致总共创建两个额外的 S 副本,一个在 f1 中,另一个用于保存 f1 的返回值。

相比之下,如果我们使用 copy elision:

// Good version
S __declspec(noinline) f2()
{S s;return s;
}void g2()
{consume(f2());
}

那么 msvc 代码生成是

; on entry, rcx says where to put the return value在入口处,rcx指出将返回值放在何处。
f2:push    rbxsub     rsp, 48mov     rbx, rcx; construct directly into return value (still in rcx)直接构造为返回值(仍在rcx中)call    S::S(); and return it并将其返回mov     rax, rbxadd     rsp, 48pop     rbxretg2:sub     rsp, 40; put return value of f1 directly into outbound parameter将f1的返回值直接放入出站参数中lea     rcx, qword ptr [rsp+48]call    f2(); call consume with the outbound parameter使用出站参数调用消费mov     rcx, eaxcall    consume(S); returnadd     rsp, 40ret

其他编译器(GCC, Clang, ICC ICX)也有类似结果。在 GCC, Clang 和 ICX 中,你可以启用 -Wpessimizing-move 警告来提示你何时犯了这些错误。


(文章评论区精选翻译)

  • 紅樓鍮鍮:std::move 滥用与 auto&& 滥用结合时会更糟:auto&& name = get_name(); 会不必要地创建一个抑制 NRVO 的引用;auto&& name = std::move(get_name()); 实际上会创建一个悬垂引用,因为 C++ 不会延长临时对象的生命周期如果在其和声明的局部引用之间存在函数调用。有趣的是,auto&& name = static_cast<std::string &&>(get_name()); 会产生一个有效的引用!我怀疑用 static_cast 替换 std::move 可能会恢复 NRVO。
  • Neil Rashbrook: (回应上条)假设我做对了,如果你移动(move)或转换(cast)了值,你就得不到优化。
  • Kevin Norris: (回应上条)有趣的是,似乎 MSVC 能用 return static_cast<T&&>(...) 优化掉移动,但 GCC 和 Clang 不能。虽然 GCC 和 Clang 会对 return std::move(...); 发出警告,但它们不会对 return static_cast<T&&>(...) 发出警告。
  • Solomon Ucko: 我从这里得到的是:std::move 是一个转换操作(cast)。它应该以与 static_cast 完全相同的谨慎态度对待。只在你能清楚地说明这个转换在形式上做了什么(特别是:为什么你期望编译器在该值被转换为右值引用时以不同方式处理它?)、为什么该用例不满足复制省略(包括但不限于 NVRO)的条件、以及被移动源对象(moved-from)之后会怎样(特别是:你应该合理确信被移动源对象永远不会再被使用)时使用它。
  • Simon Farnsworth: 注意,与 C++17 及以后的强制 NRVO 不同,Rust 的 NRVO完全是可选的。它在某些情况下甚至是不健全的,因此在某些版本中被禁用。
  • Kevin Norris: 有没有什么副作用或其他我看不到的原因,导致 return std::move(name); 的情况不能被优化掉?或者这只是标准遗漏了一个机会,而编译器被标准所约束?
  • (其他回复 Kevin): 标准要求如果返回值是纯右值(prvalue)则必须省略拷贝,但 std::move(foo) 是一个将亡值(xvalue)。标准允许在 Raymond 描述的 NVRO 情况以及其他一些涉及异常和协程的特殊场景中进行省略。在所有其他情况下,省略只允许在 “as-if” 规则下,这要求编译器证明在可观察行为上没有差异——这通常很困难。广义上将亡值情况下的省略可能无效,因为对象可能在函数调用前就存在,且对其他部分可见或并发访问,跳过其析构会造成问题。标准可以为“误用了 std::move() 的 NVRO 情况”开特例,但告诉人们不要那样做更简单。

核心总结:

Raymond Chen 的文章核心警告了在 C++ 中 过度和不必要地使用 std::move 反而会损害性能,特别是在涉及函数返回值和初始化新对象时,因为它会阻止编译器进行关键的优化:

  1. 在函数返回值上过度使用 std::move
    • 错误做法: return std::move(local_variable);
    • 危害: 阻止了 命名返回值优化 (NRVO)。NRVO 允许编译器直接在函数的返回值槽中构造局部变量,从而完全省略拷贝/移动构造
    • 正确做法: 直接返回局部变量:return local_variable;。这满足 NRVO 的条件,编译器(在实践中通常会)进行优化,实现零拷贝/移动。
  2. 在接收函数返回值初始化新对象时过度使用 std::move:
    • 错误做法 (初始化变量): T var = std::move(func());
    • 错误做法 (传递参数): some_func(std::move(func()));
    • 危害: 阻止了 初始化省略 (Initialization Elision)。当使用相同类型的值初始化另一个对象(变量或参数)时,编译器可以直接将源值用作目标对象,省略中间的临时对象和拷贝/移动操作
    • 正确做法: 直接使用返回值初始化:T var = func();some_func(func());。这允许编译器进行初始化省略。
  3. 双重错误(最糟糕): 如果在返回函数中错误使用 std::move 阻止了 NRVO,并且在调用函数中错误使用 std::move 阻止了初始化省略,那么本来可以完全零拷贝/移动的操作链(func() 内部构造 -> 直接用作返回值 -> 直接用作参数或变量),会变成:
    • 在返回函数中:一次移动构造(因为 NRVO 被阻止)。
    • 在调用函数中:创建一个临时对象(存储 func() 的返回值),然后通过移动构造初始化目标变量或参数(因为初始化省略被阻止)。
    • 结果:创建了两个额外的临时对象,发生了两次额外的移动操作,并进行了两次额外的析构操作,性能显著下降。

核心教训与建议:

  • 优先信任编译器优化: 在简单返回局部变量或直接用返回值初始化相同类型对象时,不要添加 std::move。让编译器利用 NRVO 和初始化省略规则进行零拷贝优化。
  • std::move 视为强制类型转换: 像对待 static_cast 一样谨慎使用 std::move。仅在需要显式启用移动语义(例如,要将对象的所有权转移给函数,或你知道源对象不再需要且移动比拷贝更廉价)时使用。
  • 理解使用 std::move 的后果: 使用 std::move 后,被移动的对象处于有效但未指定状态,不应再依赖其内容
  • 启用编译器警告: 使用 GCC, Clang 或 ICC 时,开启 -Wpessimizing-move(或等效警告)来检测这种潜在的性能反优化。
  • 避免 auto&&std::move 的致命组合: auto&& name = std::move(func()); 容易创建悬垂引用,因为 func() 返回的临时对象生命周期不会因 std::move 而延长。

总之: std::move 是一个有用的工具,但滥用它会适得其反,阻碍编译器进行更高效的优化(复制省略),最终导致性能下降和潜在问题。在简单返回局部变量和直接初始化场景中,应首选简洁写法,信任编译器优化。







原文翻译

The C++ std::move function casts its parameter to an rvalue reference, which enables its contents to be consumed by another operation. But in your excitement about this new expressive capability, take care not to overuse it.

C++ std::move 函数将其参数强制转换为右值引用,从而使其内容可供其他作使用。但是,在您对这种新的表达能力感到兴奋时,请注意不要过度使用它。

std::string get_name(int id)
{std::string name = std::to_string(id);/* assume other calculations happen here 假设这里发生了其他计算*/return std::move(name);
}

You think you are giving the compiler some help by saying “Hey, like, I’m not using my local variable name after this point, so you can just move the string into the return value.”

您认为您通过说“嘿,比如,在这一点之后我没有使用我的局部变量名称 ,所以你可以将字符串移动到返回值中”来为编译器提供一些帮助。

Unfortunately, your help is actually hurting. Adding a std::move causes the return statement to fail to satisfy the conditions for copy elision (commonly known as Named Return Value Optimization, or NRVO): The thing being returned must be the name of a local variable with the same type as the function return value.

不幸的是,你的帮助实际上是有害的。添加 std::move 会导致 return 语句无法满足复制省略的条件 (通常称为命名返回值优化,或 NRVO):返回的事物必须是与函数返回值类型相同的局部变量的名称。

The added std::move prevents NRVO, and the return value is move-constructed from the name variable.

添加的 std::move 会阻止 NRVO,并且返回值是从 name 变量移动构造的。

std::string get_name(int id)
{std::string name = std::to_string(id);/* assume other calculations happen here 假设这里发生了其他计算*/return name;
}

This time, we return name directly, and the compiler can now elide the copy and put the name variable directly in the return value slot with no copy. (Compilers are permitted but not required to perform this optimization, but in practice, all compilers will do it if all code paths return the same local variable.)

这一次,我们直接返回 name,编译器现在可以省略 copy 并将 name 变量直接放在没有 copy 的返回值槽中。(允许但不要求编译器执行此优化,但实际上,如果所有代码路径都返回相同的局部变量,则所有编译器都会执行此作。

The other half of the overzealous std::move is on the receiving end.

过分热心的 std::move 的另一半在接收端。

extern void report_name(std::string name);void sample1()
{std::string name = std::move(get_name());
}void sample2()
{report_name(std::move(get_name()));
}

In these two sample functions, we take the return value from get_name and explicitly std::move it into a new local variable or into a function parameter. This is another case of trying to be helpful and ending up hurting.

在这两个示例函数中,我们从 get_name 获取返回值,并显式地 std::move 将其放入新的局部变量或函数参数中。这是另一种试图提供帮助但最终受伤的情况。

Constructing a value (either a local variable or a function parameter) from a matching value of the same type will be elided: The matching value is stored directly into the local variable or parameter without a copy. But adding a std::move prevents this optimization from occurring, and the value will instead be move-constructed.

从相同类型的匹配值构造值(局部变量或函数参数)将被省略:匹配值直接存储到局部变量或参数中,无需复制。但是添加 std::move 会阻止这种优化的发生,并且该值将被 move 构造。

extern void report_name(std::string name);void sample1()
{std::string name = get_name();
}void sample2()
{report_name(get_name());
}

What’s particularly exciting is when you combine both mistakes. In that case, you took what would have been a sequence that had no copy or move operations at all and converted it into a sequence that creates two extra temporaries, two extra move operations, and two extra destructions.

特别令人兴奋的是,当你把这两个错误结合起来时。在这种情况下,您获取了一个根本没有复制或移动作的序列,并将其转换为一个序列,该序列创建了两个额外的临时作、两个额外的移动作和两个额外的销毁。

#include <memory>
struct S
{S();S(S const&);S(S &&);~S();
};extern void consume(S s); // 消耗// Bad version
S __declspec(noinline) f1()
{S s;return std::move(s);
}void g1()
{consume(std::move(f1()));
}

Here’s the compiler output for msvc:

以下是 msvc 的编译器输出:

; on entry, rcx says where to put the return value在入口处,rcx指出将返回值放在何处。
f1:mov     qword ptr [rsp+8], rcxpush    rbxsub     rsp, 48mov     rbx, rcx; construct local variable s on stack在堆栈上构造局部变量slea     rcx, qword ptr [rsp+64]call    S::S(); copy local variable to return value复制局部变量到返回值lea     rdx, qword ptr [rsp+64]mov     rcx, rbxcall    S::S(S &&); destruct the local variable s析构局部变量 slea     rcx, qword ptr [rsp+64]call    S::~S(); return the result返回结果mov     rax, rbxadd     rsp, 48pop     rbxretg1:sub     rsp, 40; call f1 and store into temporary variable调用f1并存储到临时变量中lea     rcx, qword ptr [rsp+56]call    f1(); copy temporary to outbound parameter复制临时到出站参数mov     rdx, raxlea     rcx, qword ptr [rsp+48]call    S::S(S &&); call consume with the outbound parameter使用出站参数调用消费mov     rcx, raxcall    consume(S); clean up the temporary清理临时的lea     rcx, qword ptr [rsp+56]call    S::~S(); returnadd     rsp, 40ret

Notice that calling g1 resulted in the creation of a total of two extra copies of S, one in f1 and another to hold the return value of f1.

请注意,调用 g1 会导致总共创建两个额外的 S 副本,一个在 f1 中,另一个用于保存 f1 的返回值。

By comparison, if we use copy elision:

相比之下,如果我们使用 copy elision:

// Good version
S __declspec(noinline) f2()
{S s;return s;
}void g2()
{consume(f2());
}

then the msvc code generation is

那么 msvc 代码生成是

; on entry, rcx says where to put the return value在入口处,rcx指出将返回值放在何处。
f2:push    rbxsub     rsp, 48mov     rbx, rcx; construct directly into return value (still in rcx)直接构造为返回值(仍在rcx中)call    S::S(); and return it并将其返回mov     rax, rbxadd     rsp, 48pop     rbxretg2:sub     rsp, 40; put return value of f1 directly into outbound parameter将f1的返回值直接放入出站参数中lea     rcx, qword ptr [rsp+48]call    f2(); call consume with the outbound parameter使用出站参数调用消费mov     rcx, eaxcall    consume(S); returnadd     rsp, 40ret

You get similar results with gcc, clang, and icc icx.

使用 gcc、clang 和 icc icx 可以得到类似的结果。

In gcc, clang, and icx, you can enable the pessimizing-move warning to tell you when you make these mistakes.

在 gcc、clang 和 icx 中,您可以启用 pessimizing-move 警告,以便在您犯这些错误时通知您。

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

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

相关文章

Ubuntu24.04 onnx 模型转 rknn

前面的环境配置有点懒得写&#xff0c;教程也很多&#xff0c;可以自己找 rknn-toolkit2 gitee 地址&#xff1a;pingli/rknn-toolkit2 试了很多开源的代码&#xff0c;都没办法跑通&#xff0c; 最后自己改了一版 微调后的 qwen2 模型适用 from rknn.api import RKNN impor…

Electron通信流程

前言 今天讲Electron框架的通信流程&#xff0c;首先我们需要知道为什么需要通信。这得益于Electron的多进程模型&#xff0c;它主要模仿chrome的多进程模型如下图&#xff1a; 作为应用开发者&#xff0c;我们将控制两种类型的进程&#xff1a;主进程和渲染器进程 。 …

uni-app项目实战笔记1--创建项目和实现首页轮播图功能

ps:本笔记来自B站咸虾米壁纸项目 一.创建项目&#xff0c;完成项目初始化搭建 1.在HBuilder X创建wallper项目&#xff0c;使用默认模块&#xff0c;选择vue&#xff1b; 2.在项目根目录下创建common目录&#xff0c;用于存放静态资源&#xff0c;创建项目时自动生成static目…

机械制造系统中 PROFINET 与 PROFIBUS-DP 的融合应用及捷米科技解决方案

在机械制造领域&#xff0c;工业通信网络的兼容性与灵活性直接影响产线的自动化水平与生产效率。当前&#xff0c;多数机械制造系统采用PROFINET 控制器构建核心网络架构&#xff0c;并通过微波无线连接实现设备互联。随着工业网络的发展&#xff0c;系统中常需同时集成PROFINE…

MCP 协议系列序言篇:开启 AI 应用融合新时代的钥匙

文章目录 序言&#xff1a;AI 应用层进入 MCP 时代为什么 MCP 开启 AI 应用融合新时代的钥匙为什么是 MCP&#xff1f;它与 Function Calling、Agent 有什么区别&#xff1f;Function CallingAI AgentMCP&#xff08;Model Context Protocol&#xff09; MCP 如何工作MCP Serve…

【threejs】每天一个小案例讲解:光照

代码仓 GitHub - TiffanyHoo/three_practices: Learning three.js together! 可自行clone&#xff0c;无需安装依赖&#xff0c;直接liver-server运行/直接打开chapter01中的html文件 运行效果图 知识要点 常见光照类型及其特点如下&#xff1a; 1. 环境光&#xff08;Ambi…

大模型在输尿管下段积水预测及临床应用的研究

目录 一、引言 1.1 研究背景与意义 1.2 研究目的 1.3 研究范围与限制 1.4 文献综述 1.5 研究方法和框架 二、相关理论与概念 2.1 大模型技术原理 2.2 输尿管下段积水病理机制 2.3 大模型在医学预测领域的应用 三、大模型预测输尿管下段积水的方法 3.1 数据收集 3.…

gitlab相关操作

2025.06.11今天我学习了如何在终端使用git相关操作&#xff1a; 一、需要修改新的仓库git地址的时候&#xff1a; &#xff08;1&#xff09;检查当前远程仓库 git remote -v 输出示例&#xff1a; origin https://github.com/old-repo.git (fetch) origin https://github.c…

51c自动驾驶~合集58

我自己的原文哦~ https://blog.51cto.com/whaosoft/13967107 #CCA-Attention 全局池化局部保留&#xff0c;CCA-Attention为LLM长文本建模带来突破性进展 琶洲实验室、华南理工大学联合推出关键上下文感知注意力机制&#xff08;CCA-Attention&#xff09;&#xff0c;…

通过共享内存在多程序之间实现数据通信

注&#xff1a;以下内容为与 GPT-4O 共同创作完成 以共享内存的方式实现多程序之间的数据通信&#xff0c;尤其适合在一台机器上的多程序之间进行高频数据交换。 以下示例展示了 sender.py 向 receiver.py 发送数据并接收经 receiver.py 处理后的数据&#xff0c;以及如何通过…

[论文阅读] 人工智能+软件工程 | 理解GitGoodBench:评估AI代理在Git中表现的新基准

理解GitGoodBench&#xff1a;评估AI代理在Git中表现的新基准 论文信息 GitGoodBench: A Novel Benchmark For Evaluating Agentic Performance On Git Tobias Lindenbauer, Egor Bogomolov, Yaroslav Zharov Cite as: arXiv:2505.22583 [cs.SE] 研究背景&#xff1a;当AI走进…

开源 java android app 开发(十二)封库.aar

文章的目的为了记录使用java 进行android app 开发学习的经历。本职为嵌入式软件开发&#xff0c;公司安排开发app&#xff0c;临时学习&#xff0c;完成app的开发。开发流程和要点有些记忆模糊&#xff0c;赶紧记录&#xff0c;防止忘记。 相关链接&#xff1a; 开源 java an…

ubuntu + nginx 1.26 + php7.4 + mysql8.0 调优

服务器配置 8核 16G 查看内存 free -h nginx配置 worker_processes auto; # 自动检测CPU核心数 worker_rlimit_nofile 65535; # 提高文件描述符限制 ​ events {worker_connections 8192; # 每个worker的最大连接数multi_accept on; # 一次性接受…

[未验证]abaqus2022 更改内置python

如何在 Abaqus 2022 中更改内置 Python 在 Abaqus 中&#xff0c;Python 是常用的脚本语言&#xff0c;它使得用户能够自动化模型的创建、分析和后处理。可能有时候你需要更改默认的 Python 版本&#xff0c;比如使用特定库或者功能。本文将为您详细说明如何在 Abaqus 2022 中更…

RAG文档解析难点2:excel数据“大海捞针”,超大Excel解析与精准行列查询指南

写在前面 在构建检索增强生成(RAG)应用时,Excel文件是不可或缺的数据源。它们通常包含了企业运营、市场分析、科学研究等各个领域的宝贵数据。然而,当这些Excel文件变得“超大”——可能包含数十万甚至数百万行数据时,传统的解析方法和RAG数据处理流程将面临严峻的内存、…

深度掌控,智启未来 —— 基于 STM32F103RBT6 的控制板

在科技浪潮奔涌向前的时代&#xff0c;电子领域的创新发展从未停歇。对于电子工程师、科研工作者以及电子技术爱好者&#xff0c;在校电子专业学生而言&#xff0c;一款性能卓越、功能全面且稳定可靠的开发板&#xff0c;是探索电子世界奥秘、实现创意构想的关键基石。今天&…

什么样的登录方式才是最安全的?

目录 一、基础协议&#xff1a;HTTP与HTTPS HTTP协议 HTTPS协议 二、常见Web攻击与防御 2.1 XSS 常见攻击手段 针对XSS 攻击窃取 Cookie 2.2 CSRF CSRF攻击的核心特点 与XSS的区别 常见防御措施 三、疑问解答 四、登录方式演变 4.1 方案一&#x1f436;狗都不用 …

android studio底部导航栏

实现底部导航栏切换 将java文件return的xml文件赋值给页面FrameLayout控件 java文件BottomNavigationView&#xff0c;监听器setOnNavigationItemSelectedListener MainActivity.java代码 package com.example.myapplication;import android.os.Bundle;import androidx.appc…

vue-router相关理解

一、前言 随着 Vue.js 在前端开发中的广泛应用&#xff0c;Vue Router 成为了 Vue 官方推荐的路由管理器。它不仅支持单页面应用&#xff08;SPA&#xff09;中常见的路由跳转、嵌套路由、懒加载等功能&#xff0c;还提供了导航守卫、动态路由等高级特性。 本文将带你深入了解…

uni-app 自定义路由封装模块详解(附源码逐行解读)

&#x1f680;uni-app 自定义路由封装模块详解&#xff08;附源码逐行解读&#xff09; &#x1f4cc; 请收藏 点赞 关注&#xff0c;获取更多 uni-app 项目实用技巧&#xff01; 在实际 uni-app 项目中&#xff0c;我们常常需要对 uni.navigateTo、uni.switchTab 等 API 做…