AddressSanitizer(ASan)的使用

ASan 是GCC 和Clang 编译器的一部分,所以无需单独安装.

AddressSanitizer与Valgrind相比

  1. Valgrind通过模拟CPU来检测内存错误,导致会以较慢的速度运行程序;而AddressSanitizer是在编译阶段插入检查的逻辑,执行速度比Valgrind快很多
  2. Valgrind是一个独立的工具,可以使用在任何程序上;而AddressSanitizer与编译器紧密集成,可以在构建时自动启用
  3. 在错误信息的展示上,AddressSanitizer提供的错误信息比Valgrind容易理解,但Valgrind更详细
  4. AddressSanitizer作为编译器的一部分,通过编译选项启用;而Valgrind作为独立的工具,需要更多的配置和学习才能使用
  5. AddressSanitizer通过编译时插桩和运行时检查来检测内存错误,误报率较低

AddressSanitizer能检测的错误类型
截图 2025-07-09 09-35-44.png

CMake中的设置:

1
2
3
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address")

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address")

比如这样的典型代码

1
Test* t = new Test();

ASan的报错如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
=================================================================

==2816467==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 8 byte(s) in 1 object(s) allocated from:

#0 0x72cdcdefe548 in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:95

#1 0x5cbc2c68d2c3 in main /home/zzp/qt_projects/untitled/main.cpp:10

#2 0x72cdcd62a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58

#3 0x72cdcd62a28a in __libc_start_main_impl ../csu/libc-start.c:360

#4 0x5cbc2c68d1e4 in _start (/home/zzp/qt_projects/build-untitled-Desktop_Qt_5_9_9_GCC_64bit-Debug/untitled+0x21e4) (BuildId: 1061b9fe4580a7e3af28420ed615c3664d393550)

SUMMARY: AddressSanitizer: 8 byte(s) leaked in 1 allocation(s).


Groot的使用

缺点

  1. 对一个控制节点,用Create Subtree创建子树后,无法恢复不是子树的状态。在左侧的树列表里,千万不要删除任何树,否则无法恢复
  2. 有的人在groot里添加SetBlackboard设置黑板变量,有的人是在C++里设置,而平时看行为树逻辑一般都在groot里,会不知道C++里做了什么
  3. 如果当前行为树设计有错,比如有多余节点,groot无法使用保存按钮
  4. Switch最多有6个case,如果switch多设置了节点,Groot还不能报错
  5. 非Pro版本无法搜索节点,而且不能搜索节点的输入输出接口,注释等内容,只能按节点名称搜索
  6. 新建空文件 —-> 保存文件,结果出现对话框: Please initalize new files on the disk before saving the project。这个现象说明了groot这个软件设计非常失败,我从没见过有哪个软件新建一个文件后还不能直接保存的,更可笑的是初始化的单词都是错的。这么低级的缺陷都有,还好意思对pro版本收费?需要到左侧的根节点,右键save保存,以后才能用保存按钮。
  7. 有时打开一个文件时间长了,拖动时发现不是手形的光标了,变成普通光标
  8. 只有Ctrl+Z,没有Ctral+Y。使用时不太方便
  9. 缺少资料
  • Groot可以跨文件复制节点,跨版本也可以

Real Time Monitor Mode

目前只有Groot2 Pro版本可以使用

在版本4里,BT::PublisherZMQ已经消失了。取而代之的是BT::Groot2Publisher,可以这样用

1
2
3
4
5
6
7
8
9
10
factory.registerBehaviorTreeFromFile("/home/user/test.xml");
auto tree = factory.createTree("test", maintree_bb);
// createTree之后,tick之前
BT::Groot2Publisher publisher(tree);

while (true)
{
tree.tickOnce();
tree.sleep(std::chrono::milliseconds(100) );
}

打开groot之后,点击Connect毫无反应,官方说明没有帮助,读源码发现构造函数里有默认端口号:Groot2Publisher(const BT::Tree& tree, unsigned server_port = 1667);

使用1667后就成功了。Host需要看情况作修改

截图 2025-07-05 16-40-03.png
参考: BT::Groot2Publisher Class Reference


行为树的节点

行为树是由控制节点、装饰节点、行为节点组成的一棵树。中间节点一般为控制节点和装饰节,用于控制行为树的执行流程,它们相对固定,一旦确定几乎不会变化。叶子节点由Action节点或Condition节点组成,使用者的大部分工作都是设计树的逻辑和行为节点。

节点的状态

行为树的每个节点都有一个返回值,平常用的最多的是:

  • 成功(Success)
  • 失败(failed)
  • 运行中(running):表示当前帧没有执行完成、下帧继续执行。

完整枚举值:

1
2
3
4
5
6
7
8
enum class NodeStatus
{
IDLE = 0,
RUNNING = 1,
SUCCESS = 2,
FAILURE = 3,
SKIPPED = 4,
};

节点调用status()可以获得当期的运行状态,返回值是NodeStatus。教程里说状态不能是IDLE,但我的行为树节点里调用status()常常发现状态就处在 IDLE,不明白为什么。

控制类节点

控制节点一般为中间节点,用于控制行为树的执行流程,决定了其子节点是以顺序、并行、随机或其它方式执行。

常用:AsyncFallback, AsyncSequence, Fallback, IfThenElse, Parallel, ParallelAll, ReactiveFallback, ReactiveSequence, Sequence, SequencewithMemory, Switch, WhileDoElse

  • 顺序节点

依次执行所有子节点,若当前子节点返回成功,则继续执行下一个子节点;若子当前节点返回失败,则中断后续子节点的执行,并把结果返回给父节点。节点1返回成功,继续执行节点2;节点2返回失败,则把结果返回给Sequences的父节点,节点3并不会执行。顺序节点相当于and语义。

  • 选择节点(Selector)

依次执行所有子节点,若当前子节点返回成功,则中断后续节点运行,并把结果返回给父节点。相当于or语义

当有的行为节点对应的代码执行较长,例如:播放动画,此时,这个行为节点会向父节点返回running,于是选择节点便不再执行后续节点,直接向父节点返回 running

  • 并行节点(Parallel)

依次执行所有子节点,无论失败与否,都会把所有子节点执行一遍。至于Parallel节点该返回什么值给父节点,这要看需求。比如:成功数 > 失败数返回成功,否则返回失败。

  • 随机节点(Random)

随机选择一个子节点来运行。机器人领域应该不会使用。在游戏设计里,AI角色每天会根据自己的心情选择是呆在家里、工作或是出门游玩,可以采用随机选择节点

  • 记忆节点(MemSequences、MemSelector)

使用很少。功能和顺序节点、选择节点类似,唯一不同是会保存当前执行进度(比如:保存当前子节点索引),下一帧继续执行当前节点,如果当前节点是中间节点,则会跳过前面的节点。

修饰节点(Decorator)

常用:Delay, ForceFailure, ForceSuccess, Inverter, KeepRunningUntilFailure, LoopDouble, LoopString, Precondition, Repeat, RetryUntilSuccessful, RunOnce, Timeout

  • 逆变节点(Inverter):tick孩子节点一次,孩子节点失败返回SUCCESS,成功返回FAILURE,孩子节点运行则返回RUNNING。也就是对子节点的返回值取反,相当于not语义,它只会有一个子节点
  • 成功节点 (ForceSuccessNode):tick子节点一次,不管其子节点返回何值,都会返回Success给父节点
  • 重复节点 (Repeater):重复执行n次子节点(n作为数据输入),直到子节点返回失败,则该节点返回FAILURE,若子节点返回RUNNING ,则同样返回RUNNING。
  • RetryNode: 最多tick子节点 n 次,(n作为数据输入),直到子节点返回成功,则该节点返回 SUCCESS,若子节点返回RUNNING ,则同样返回RUNNING
  • 执行一段时间(MaxTime):重复执行子节点一段时间

修饰节点RunOnce在调试时很有用,可以让某黑板变量赋值,然后让行为树开始运行,但又不会一直循环运行

action

动作节点通常实现服务客户端和动作客户端,也可以是一些简单的执行程序。action通常作为行为树中的叶子节点,负责具体行为和功能的实现。但这些具体的功能代码并没有在叶子节点中而是在对应的服务端。执行这种节点,可能只需要一帧就可以完成,也可能需要多帧才能完成。

它至少包含两个函数:

  • Init:用于初始化节点,比如读取配置数据初始化当前节点,只会执行一次。
  • OnTick:每一帧都会执行,节点的主要逻辑都在此函数中实现或调用。

常用: AlwaysSuccess, Script, SetBlackboard, sleep

condition

这是条件控制节点。比如判断电池电量,某一开关信号等等。


Nav2框架

安装

依赖项

1
2
3
4
5
6
7
8
9
10
sudo apt install -y ros-jazzy-tf2*
rm ~/.config/ros.org/rqt_gui.ini
# 重启rqt,然后再rqt中plugin->visualization中可以看到到 tf tree

sudo apt-get install gazebo-ros-pkgs

sudo apt install -y ros-jazzy-navigation2
sudo apt install -y ros-jazzy-nav2-bringups
sudo apt install -y ros-jazzy-turtlebot3*
sudo apt install -y ros-jazzy-test-msgs ros-jazzy-geographic-msgs

下载jazzy分支的Navigation2,一般能正常编译。

在Nav2存储库的根目录中运行doxygen。它将生成一个包含文档的/doc/*目录。文档的入口点在浏览器中是index.html。

运行 Nav2 框架

使用 turtlebot3: ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py

不需要设置 export GZ_SIM_RESOURCE_PATH

  • 手动控制: ros2 run turtlebot3_teleop teleop_keyboard


  • 建图程序: ros2 launch turtlebot3_cartographer cartographer.launch.py use_sim_time:=True


  • 保存地图: ros2 run nav2_map_server map_saver_cli -f test


  • 导航程序: ros2 launch turtlebot3_navigation2 navigation2.launch.py use_sim_time:=True map:=maps/test.yaml

节点和话题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/amcl

/behavior_server
/bt_navigator
/bt_navigator_navigate_through_poses_rclcpp_node
/bt_navigator_navigate_to_pose_rclcpp_node

/collision_monitor
/controller_server
/docking_server
/global_costmap/global_costmap
/local_costmap/local_costmap

/launch_ros_3856071
/lifecycle_manager_localization
/lifecycle_manager_navigation
/map_server
/nav2_container
/planner_server
/robot_state_publisher
/ros_gz_bridge
/ros_gz_image
/rviz2
/rviz_navigation_dialog_action_client

/smoother_server
/velocity_smoother
/waypoint_follower

速度输出是 /cmd_vel_smoothed

问题

  1. 因为显卡的驱动问题,有时加载不出机器人的模型

ROS2中的日志和rqt等工具

日志

1
2
3
4
5
RCLCPP_DEBUG(get_logger(), "我是DEBUG级别的日志,我被打印出来了!");
RCLCPP_INFO(get_logger(), "我是INFO级别的日志,我被打印出来了!");
RCLCPP_WARN(get_logger(), "我是WARN级别的日志,我被打印出来了!");
RCLCPP_ERROR(get_logger(), "我是ERROR级别的日志,我被打印出来了!");
RCLCPP_FATAL(get_logger(), "我是FATAL级别的日志,我被打印出来了!");

运行节点时,可以指定日志的最低等级: ros2 run lpackage_name executable_name --ros-args --log-level info。 可选等级: debug, warn, fatal

日志默认都没有颜色,要加颜色需要 export RCUTILS_COLORIZED_OUTPUT=1

设置日志的格式: export RCUTILS_CONSOLE_OUTPUT_FORMAT="[{severity} {date_time_with_ms}] [{name}]: {message}"


rqt_graph 能够可视化节点和主题之间的连接。这个命令和ROS1一样,也是ROS2为数不多的GUI可视界面


ros2doctor可以检测大概的ros2整体配置,ros2doctor并不是一个调错的工具,对你的代码调试没有帮助. 它一般查找你系统中警告部分,只有UserWarning: ERROR:开头的可能是错误。另外可以检查发布者没有订阅者的问题。

运行 ros2 doctor 或者 ros2 doctor --report


ROS2中的tf

tf

  • ros2 run tf2_tools view_frames

  • ros2 run tf2_ros tf2_echo source_frame target_frame

  • ros2 run tf2_ros static_transform_publisher x y z yaw pitch roll frame_id child_frame_id

  • ros2 run tf2_ros static_transform_publisher x y z qx qy qz qw frame_id child_frame_id

运行结果:

1
2
3
4
5
6
7
8
9
10
At time 197.27000000
- Translation: [0.008, -0.000, 0.064]
- Rotation: in Quaternion [-0.000, -0.001, -0.010, 1.000]
- Rotation: in RPY (radian) [-0.000, -0.002, -0.020]
- Rotation: in RPY (degree) [-0.001, -0.092, -1.160]
- Matrix:
1.000 0.020 -0.002 0.008
-0.020 1.000 0.000 -0.000
0.002 -0.000 1.000 0.064
0.000 0.000 0.000 1.000


DDS和Qos

DDS 基于 Topic 来实现发布-订阅模式,而且没有中心节点。是一种很好的端到端分布式通信中间件

DDS 本身并没有定义应该如何发现各节点,而是在 RTPS 中定义该行为。RTPS是 DDS 底层使用的协议,用来保证各种 DDS 的实现之间可以互操作。在 RTPS 中定义了两个独立的发现协议,分别是 PDP(Participant Discovery Protocol) 和 EDP(Endpoint Discovery Protocol),PDP 用于发现各参与节点,EDP 用于发现每个节点提供的所有端点(Endpoints)

ROS2的发布订阅代码里,要指定QoS的历史深度

1
2
rclcpp::QoS::QoS(size_t  history_depth)
QoS(KeepLast(history_depth))

DDS 的发布者和订阅者是解耦的,即发布者不知道谁订阅它发布的数据,反之亦然。DDS 支持动态发现,即节点可以在不需要手动配置的情况下动态发现其他节点及其发布的主题。

QoS策略

无人机传图像到电脑端,可以用best effort模式,可以接受丢帧,但注重实时性。如果用手柄操作无人机,用reliable模式,不能丢失传送数据

DDS 的核心特性之一,它允许用户配置数据传输的行为。常见的 QoS 策略包括:

  1. 可靠性 (Reliability)

RELIABLE:确保每条消息都被成功传输。如果消息未成功传输,会尝试重发。
BEST_EFFORT:没有重发机制,传输失败时不会重试,适用于对数据丢失容忍较高的场景。

  1. 历史 (History)

KEEP_LAST:只保留最新的 N 条消息,队列满时会丢弃最旧的消息。
KEEP_ALL:保持所有历史消息,直到订阅者处理完所有消息。

  1. 队列大小 (Depth)

设置消息队列的长度。如果队列满了,新的消息将根据历史策略被丢弃或覆盖。一般来说,depth 越大,存储的消息越多,可能增加延迟。

  1. 截止时间 (Deadline)

设置消息传输的最大延迟时间。可以用于确保消息的时效性。

  1. 寿命 (Lifespan)

设置消息的有效时间,超时后消息会被丢弃。

  1. 可靠度(Reliability)和发送频率

控制消息的传输方式及频率,常用来减少带宽消耗。

  • 优先级:为不同类型的数据设置不同的优先级。
  • 持久性:确定数据是否会在订阅者连接之前持久保存。

  • 数据过滤和选择(Content-Filtered Topics)

DDS 支持对订阅数据的 内容过滤,即订阅者只接收满足某些条件的数据。这对于减少不必要的数据传输和提高系统效率非常重要。

  • 动态发现

DDS 节点能够自动发现网络中的其他节点和它们所发布的主题。这种机制使得在一个动态变化的系统中,节点可以不依赖静态配置而自动连接和通信。

==注意Fast RTPSFast DDS是同一个东西,不同的叫法

rmw的意思是ROS Middleware Interface

共享内存

ROS的底层通信都是基于XML-RPC协议实现的,以XML-RPC的方式传输数据存在一定的延时和阻塞。在数据量小、频率低的情况下,传输耗费的时间可以忽略不计。但当传输图像和点云等数据量较大的消息,或者执行有一定的实时性要求的任务时,因传输而耗费的时间就不得不考虑。Nodelet包就是为改善这一状况设计的,它提供一种方法,可以让多个算法程序在一个进程中用shared_ptr实现零拷贝通信,以降低因为传输大数据而损耗的时间。将多个node捆绑在一起,使用pluginlib管理,使得同一个manager里面的topic的数据传输更快。

ROS2提供借用消息,允许用户的程序从RMW实现借用消息内存,以消除ROS2应用程序和RMW实现之间的数据复制。此外,rmw_fastrtps通过Fast DDS提供共享内存传输和数据共享传递机制,以加快主机内通信。结合这两个功能(消息借用和数据共享),可以实现零拷贝消息传递管道,从而显著提高ROS2应用程序的性能。

在 rclcpp 中,loaned_message 是一种用于高效消息传递的机制。它允许节点直接从订阅者队列中借用一个消息,而不是复制它。这种机制在处理大型数据或需要高性能的场景中非常有用,因为它可以减少内存拷贝和相关的开销。使用完毕后,应该尽快归还消息,以便订阅者队列可以继续处理其他消息。

默认情况下,rmw_fastrtps_cpp和rmw_fastrts_dynamics_cpp都使用共享内存传输进行主机内通信,同时使用基于网络的传输 (UDPv4)进行主机间消息传递。

为了实现零复制消息传递,应用程序需要启用快速DDS数据共享机制,并使用Loaned Messages API:

要在Iron Irwini或更高版本中启用借用消息,唯一的要求是数据类型为纯旧数据。对于Humble,除了POD类型外,还需要启用快速DDS数据共享。

要启用快速DDS数据共享传递机制,需要加载以下XML配置文件,并且需要将环境变量RMW_FASTPS_USEQOSFROM.XML设置为1

但是FASTDDS需要配置完成零拷贝通讯

参考: # ROS2 DDS中间件


ROS2节点生命周期

节点有四个主要状态:

1
2
3
4
Unconfigured
Inactive
Active
Finalized

还存在6个过渡状态
1
2
3
4
5
6
Configuring
CleaningUp
ShuttingDown
Activating
Deactivating
ErrorProcessing

在转换状态中,将执行逻辑以确定转换是否成功。

有7个函数用于监督流程,它们是:

1
2
3
4
5
6
7
create
configure
cleanup
activate
deactivate
shutdown
destroy

当一个节点启动时,它处于未配置状态只处理节点的构造函数,该构造函数不应包含任何 ROS 网络设置或参数读取

在配置阶段,触发 on_configure() 方法,将设置所有参数、ROS网络接口,以及安全系统,所有动态内存的分配。在激活阶段、触发 on_activate() 方法的将激活ROS网络接口,并设置程序中的任何状态以开始处理信息。要关闭(该节点)即过渡到停用需要清理、关闭,并以最终状态结束。

有7个函数用于监督流程,它们是:

1
2
3
4
5
6
7
create
configure
cleanup
activate
deactivate
shutdown
destroy

ros2 lifecycle 系列命令

  • 获取节点状态

ros2 lifecycle get /my_lifecycle_node. 返回 unconfigured [1]

  • 转换节点的状态

转换为configure状态: ros2 lifecycle set /my_lifecycle_node configure

除了configure,还可以是 cleanup, activate, deactivate, shutdown

  • 获取节点状态转换时的信息

ros2 topic echo /my_lifecycle_node/transition_event

比如上面的状态转换过程,话题transition_event会输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
timestamp: 0
transition:
id: 0
label: ''
start_state:
id: 1
label: unconfigured
goal_state:
id: 10
label: configuring
---
timestamp: 0
transition:
id: 0
label: ''
start_state:
id: 10
label: configuring
goal_state:
id: 2
label: inactive
---

也就是状态从unconfigured转换到了configuring,再到inactive

状态转换必须按顺序,比如unconfigured,如果直接set activate,会失败,然后有提示信息。

  • 获取当前状态可转换的目标状态

ros2 lifecycle list my_lifecycle_node

  • 列出当前所有的生命节点

ros2 lifecycle nodes

service 获取当前节点的状态

ros2 service call /my_lifecycle_node/get_state lifecycle_msgs/srv/GetState

/change_state - 调用触发合法转换

/get_available_transitions - 显示合法的转换

/get_available_states - 列出所有状态

/get_transition_graph - 显示完整状态机

有用的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const State &   get_current_state ()
Return the current State

// 获得state的名称
std::string State::label() const

std::vector< State > get_available_states ()
Return a list with the available states

std::vector< Transition > get_available_transitions ()
Return a list with the available transitions

const State& trigger_transition (const Transition &transition)
Trigger the specified transition

问题

  1. map_server 切换到active状态后报错

截图 2025-07-22 10-25-19.png

应该是试图多次切换到active状态,由于已经是active,所以会报错


std常用数学函数
  • hypot()用来求三角形的斜边长,其原型为:double hypot(double x, double y);,需要#include <stdio.h>

  • fabs函数是求绝对值的函数,函数原型是extern float fabs(float x),需要#include <math.h>

对double/float数据,一定要使用fabs函数。如果用了abs,就会出现bug,因为返回是int

  • 反正切函数 atan2

atan2返回给定的 X 及 Y 坐标值的反正切值。反正切的角度值等于 X 轴与通过原点和给定坐标点 (Y坐标, X坐标) 的直线之间的夹角。结果以弧度表示并介于-pipi之间(不包括-pi)。 而atan(a/b)的取值范围介于-pi/2pi/2之间,不包括±pi/2

  • std::sin 等三角函数
  • std::fmod

计算两个浮点数相除的余数

1
2
double x = 7.5, y = 2.1;
double result = std::fmod(x, y); // 1.2

floor, ceil, round

std::floorstd::ceil都是对变量进行取整,只不过取整的方向不同。 std::floor是向下取整数,std::ceil是向上取整数。
比如输入3.6,前者输出3,后者输出4。但输入3.2,结果不变。

std::round 才是四舍五入

找最大最小

std::min(const T& a, const T& b); 求两个参数的最小值

std::max(const T& a, const T& b); 求两个参数的最大值

以下库函数需要 #include <algorithm>

minmax_element找出容器中最小和最大元素的迭代器,作为std::pair返回。时间复杂度为 O(n)

1
2
template< class ForwardIt >
std::pair<ForwardIt,ForwardIt> minmax_element( ForwardIt first, ForwardIt last );

1
2
3
4
5
6
7
8
std::vector<int> v = { 1, 2, 5, 4, 100, 0, -199, 33 };

auto result = std::minmax_element(v.begin(), v.end());
// 输出首次出现的最小元素
std::cout << "min element is: " << *result.first << '\n';

// 输出首次出现的最大元素
std::cout << "max element is: " << *result.second << '\n';

C++17 增加了min_element返回迭代器位置, 复杂度为 O(n)max_element返回迭代器位置,复杂度为 O(n)

1
2
3
4
5
6
7
8
9
std::vector<int> v{3, 1, 4, 1, 5, 9};

std::vector<int>::iterator min = std::min_element(v.begin(), v.end());
std::cout << "min element at: " << std::distance(v.begin(), min) << std::endl;
std::cout << "min value is: " << *min << std::endl;

std::vector<int>::iterator max = std::max_element(v.begin(), v.end());
std::cout << "max element at: " << std::distance(v.begin(), max) << std::endl;
std::cout << "max value is: " << *max << std::endl;


判断 inf, nan

1
2
3
4
5
bool isinf( float arg );

bool isfinite( float arg );

bool isnan( float arg );

numeric_limits

模板类,常用于提供很大很小的极值,需要#include <limits>

1
2
3
4
5
cout<<std::numeric_limits<int>::max()<<endl;
cout<<std::numeric_limits<long>::max()<<endl;

cout<<std::numeric_limits<int>::min()<<endl;
cout<<std::numeric_limits<long>::min()<<endl;

结果

1
2
3
4
2147483647
9223372036854775807
-2147483648
-9223372036854775808


C++的新类型 optional, variant, any

std::optional

std::optional<T>代表一个可能存在的T值,实际上是一种Sum Type。常用于可能失败的函数的返回值中,比如工厂函数。在C++17之前,往往使用T*作为返回值,如果为nullptr则代表函数失败,否则T*指向了真正的返回值。但是这种写法模糊了所有权,函数的调用方无法确定是否应该接管T*的内存管理,而且T*可能为空的假设,如果忘记检查则会有SegFault的风险。

使用指针和optional的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <optional>

// c++17 更安全和直观
std::optional<int> func(bool param)
{
if(param)
{
return 1;
}
return nullopt;
}

int* func2(bool param)
{
int p = 1;
int* ptr = &p;
if(param)
{
return ptr;
}
return nullptr;
}


auto f = func(false);
if(f.has_value() )
cout << f.value() << endl;

auto f2 = func2(false);
if(f2)
cout << *f2 << endl;

如果换成unique_ptr代替optional,后果是多一次堆内存分配。另外如果用了unique_ptr做类成员,这个类就不能被拷贝了,optional没有这个问题。二者的本质区别: unique_ptr是引用语义,optional是值语义

variant

std::variant 是 C++17 引入的一种类型安全的联合体,用来存储多个可能类型中的一种值,且保证使用时的类型安全。相比于传统的 union, std::variant不仅能够存储不同类型的值,还能自动管理复杂类型的构造与析构。

使用 std::variant 可以定义一个变量,该变量可以持有多种不同类型的值,但一次只能存储一种。std::variant 提供了一种类型安全的方式来处理多种类型,可以用在函数参数中以接受这些类型。

构造和赋值:您可以直接使用各种支持的类型初始化 std::variant, 并在需要时将其传递给函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
std::variant<int, float, std::string> v;  // v 可以是 int、float 或 std::string
cout << "size of v: "<<sizeof (v) << endl;
v = 42;
cout << std::get<int>(v) << endl;

v = 12.3f;
cout << std::get<float>(v) << endl;
cout << std::get<1>(v) << endl;

// 可以使用 std::holds_alternative<T>(variant) 来判断 std::variant 当前是否持有某种类型
if(std::holds_alternative<int>(v) )
std::cout << "v holds an int" << std::endl;
else
std::cout << "v doesn't holds an int" << std::endl;

try {
std::cout << std::get<int>(v); // 当前不是 int 类型,会抛出异常
}
catch (const std::bad_variant_access& e)
{
std::cout << "Wrong type access: " << e.what() << std::endl;
}

//使用 v.index() 获取当前存储值的类型在 std::variant 中的索引
std::cout << "current value index: " << v.index() << std::endl; // 0 表示 int,1 表示 float,依次类推

v = "Hello, std::variant!";
cout << std::get<std::string>(v) << endl;

std::variant 可以作为函数参数,接受多种类型的值,这些类型是在定义 std::variant 时指定的

std::variant 主要用于以下场景:

  1. 多类型返回值: 一个函数可能返回不同类型的值,例如成功返回数据,失败返回错误信息
  2. 替代 union: std::variant 是 union 的现代、安全替代品,支持更多的类型和安全检查
  3. std::variant 可以表示有限状态机的状态,每个状态用不同的类型表示

类型安全: 使用 std::visit 可确保访问的值是有效的,避免了类型错误

1
2
3
std::visit([](auto&& arg) {
std::cout << "use visit to print value: " << arg << std::endl; // 打印不同类型的值
}, v);

std::any

std::any是一个可以存储任何可拷贝类型的容器,C语言中通常使用void*实现类似的功能,与void*相比,std::any具有两点优势:

std::any更安全:在类型T被转换成void*时,T的类型信息就已经丢失了,在转换回具体类型时程序无法判断当前的void*的类型是否真的是T,容易带来安全隐患。而std::any会存储类型信息,std::any_cast是一个安全的类型转换。
std::any管理了对象的生命周期,在std::any析构时,会将存储的对象析构,而void*则需要手动管理内存。
std::any应当很少是程序员的第一选择,在已知类型的情况下,std::optional, std::variant和继承都是比它更高效、更合理的选择。只有当对类型完全未知的情况下,才应当使用std::any,比如动态类型文本的解析或者业务逻辑的中间层信息传递。