yaml-cpp的使用

使用yaml-cpp读文件

1
2
3
4
5
6
7
8
9
10
YAML::Node gps_root;
try {
gps_root = YAML::LoadFile("/home/user/111.yaml");
} catch (YAML::ParserException &ex) {
std::cerr << "gps.yaml parse failed: " << ex.what() << std::endl;
exit(-1);
} catch (YAML::BadFile &ex) {
std::cerr << "gps.yaml load failed: " << ex.what() << std::endl;
exit(-1);
}

所用函数的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Node LoadFile(const std::string& filename) {
std::ifstream fin(filename.c_str());
if (!fin) {
throw BadFile();
}
return Load(fin);
}

Node Load(std::istream& input)
{
Parser parser(input);
NodeBuilder builder;
if (!parser.HandleNextDocument(builder)) {
return Node();
}

return builder.Root();
}

如果读文件失败,会抛出异常,不会运行到Load,所以无法用IsDefined函数判断是否读文件成功。


无法到达目标时,搜索新的目标点
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
double planner::distance(double x1,double y1,double x2,double y2)
{
return sqrt( (x1-x2) * (x1-x2) + (y1-y2) * (y1-y2) );
}

bool planner::isAroundFree(unsigned int mx, unsigned int my)
{
if(mx <= 1 || my <= 1 || mx >= this->costmap_->getSizeInCellsX()-1 || my >= this->costmap_->getSizeInCellsY()-1)
return false;
int x,y;
for(int i=-1;i<=1;i++)
{
for(int j=-1;j<=1;j++)
{
x = static_cast<int>(mx) + i;
y = static_cast<int>(my) + j;
if(this->costmap_->getCost(static_cast<unsigned int>(x),static_cast<unsigned int>(y)) != costmap_2d::FREE_SPACE)
return false;
}
}
return true;
}

// tolerance:搜索半径,单位米。
// in:输入的点
// out:搜索到的可行点
void planner::getNearFreePoint(const geometry_msgs::PoseStamped in,
geometry_msgs::PoseStamped& out,
double tolerance)
{
out = in;
unsigned int grid_size = static_cast<unsigned int>(tolerance/costmap_->getResolution() + 0.5);
if(grid_size<1)
{
out = in;
return;
}

unsigned int mx0,my0;
if(costmap_->worldToMap(in.pose.position.x,in.pose.position.y,mx0,my0))
{
if(this->isAroundFree(mx0,my0))
return;
unsigned int minx,maxx,miny,maxy;
double wx = 0.0,wy = 0.0;
double min_move_cost = 10000000.0;
minx = mx0-grid_size>0?mx0-grid_size:0;
maxx = mx0+grid_size<costmap_->getSizeInCellsX()?mx0+grid_size:costmap_->getSizeInCellsX();
miny = my0-grid_size>0?my0-grid_size:0;
maxy = my0+grid_size<costmap_->getSizeInCellsY()?my0+grid_size:costmap_->getSizeInCellsY();
for(unsigned int i=minx;i<=maxx;i++)
{
for(unsigned int j=miny;j<=maxy;j++)
{
costmap_->mapToWorld(i,j,wx,wy);
double current_move_cost = this->distance(in.pose.position.x,in.pose.position.y,wx,wy);
if(!this->isAroundFree(i,j) || current_move_cost > tolerance)
continue;
if(min_move_cost > current_move_cost)
{
min_move_cost = current_move_cost;
out.pose.position.x = wx;
out.pose.position.y = wy;
}
}
}
}
}

ZeroMQ的学习

使用zmqpp-4.2.0,但是用函数zmq_version发现版本是 4.3.4

ZeroMQ在CMake的设置

1
2
3
4
5
6
7
8
9
10
11
12
cmake_minimum_required(VERSION 3.5)

project(untitled LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

include_directories(zmqpp-4.2.0/src/zmqpp)
LINK_DIRECTORIES(zmqpp-4.2.0/lib/)

add_executable(untitled main.cpp)
target_link_libraries(untitled zmq)

查看版本

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <zmq.h>
// 显示当前的zeromq版本
int main()
{
int major, minor, patch;
zmq_version(&major, &minor, &patch);
printf("Current ZeroMQ version is %d.%d.%d\n", major, minor, patch);
}

发布和订阅

pub端:

  • 创建context
  • 创建socket,设置ZMQ_PUB模式
  • bind端口
  • 循环发布消息send

sub端:

  • 创建context
  • 创建socket,设置ZMQ_SUB模式
  • connect到pub端
  • setsockopt设置ZMQ_SUBSCRIBE订阅的消息
  • 循环接收recv

注意事项:

  1. 我们在pub中bind,在sub中connect,在zmq的使用中无论是在pub还是sub中都可以bind,但是一般我们在pub中bind,在sub中connect。反之sub端可能收不到消息
  2. zmq_setsockopt – 设置zmq的socket属性,sub端必须使用此方法,否则是收不到消息的。
  3. pub端不能使用recv函数,sub端不能使用send函数
  4. pub端socket的zmq_send()函数永远不会阻塞

zmq_msg_init_size

使用一个指定的空间大小初始化ZMQ消息对象。原型int zmq_msg_init_size (zmq_msg_t *msg, size_t size);

分配任何被请求的资源来存储一个size大小字节的消息,并且初始化msg参数指定的消息,用来表示新分配到的消息。

在函数执行的时候,会选择是否把消息存储在栈里面(小消息),还是堆里面(大消息)。考虑到性能原因,函数不会清除消息数据。

永远不要直接对zmq_msg_t对象进行直接操作,而是要使用zmq_msg函数族进行操作。

zmq_msg_init(), zmq_msg_init_data()zmq_msg_init_size()这三个函数是互斥的。永远不要把一个zmq_msg_t对象初始化两次。

执行成功时返回0。否则返回 -1,并且设置errno的值为下列定义的值。

zmq_msg_data

原型void *zmq_msg_data (zmq_msg_t *msg); 返回msg参数指定的消息内容的指针。

函数执行成功返回0,否则返回 -1

参考:
zmq pub/sub使用详解


proto基本使用

安装

安装Protobuf需要两个zip文件,以 3.0.0 为例,到github下载页面下载 protoc-3.0.0-linux-x86_64.zipprotobuf-cpp-3.0.0.zip

安装前者:

1
2
3
4
5
6
7
8
9
10
# Unzip
unzip protoc-3.2.0-linux-x86_64.zip -d protoc3

sudo mv protoc3/bin/* /usr/local/bin/

sudo mv protoc3/include/* /usr/local/include/

# change owner,也可不执行
sudo chwon [user] /usr/local/bin/protoc
sudo chwon -R [user] /usr/local/include/google

再安装后者

1
2
3
4
5
6
7
8
9
10
11
12
13
# 解压后执行
./autogen.sh

./configure # 或者 ./configure --prefix=/usr

make
sudo make install
sudo ldconfig

protoc --version
# 输出:libprotoc 3.0.0

locate libprotobuf.so


所用protobuf的版本是 3.12.4

Test.proto文件:

1
2
3
4
5
6
7
8
9
syntax = "proto3";
// 包名:在生成对应的C++文件时,将被替换为名称空间,在代码中会有体现
package Test;

message Pose {
float x = 1;
float y = 2;
float yaw = 3;
}

根据proto文件生成.h 和 .cc文件

1
./protoc --proto_path=/home/user/qt_projects --cpp_out=/home/user/qt_projects/proto_gen /home/user/qt_projects/Test.proto

简略形式: protoc message.proto --cpp_out=.


CMake中的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cmake_minimum_required(VERSION 3.5)

project(test_proto LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

include_directories(/home/user/zmqpp-4.2.0/src/zmqpp)
include_directories(/home/user/protobuf/src)

LINK_DIRECTORIES(/home/user/x86/zmqpp-4.2.0/lib/)
LINK_DIRECTORIES(/home/user/x86/protobuf/lib)
# 上面生成的cc文件
add_executable(test_proto main.cpp Test.pb.cc)
target_link_libraries(test_proto zmq protobuf)

注意 #include "Test.pb.h"

可以这样使用

1
2
3
4
Test::Pose*  pose_msg = new Test::Pose();
pose_msg->set_x(1.5);
pose_msg->set_y(2.7);
pose_msg->set_yaw(0);


ccache加速编译

ccache

先使用apt-get install ccache进行安装。

1
2
3
4
sudo vim /etc/environment
PATH='/usr/lib/ccache:keep the rest'

source /etc/environment

输入which gcc,应该得到/usr/lib/ccache/gcc

修改CMakeLists.txt

1
2
set(CMAKE_CXX_COMPILER_LAUNCHER ccache)
set(CMAKE_C_COMPILER_LAUNCHER ccache)

但是在ROS环境下,没发现起作用

distcc

Setting up distcc (server)

So, go ahead and install distcc on the server computer (in my case, the desktop)

And then spin up the server. (The following assumes I’m on the 10.8.33.0 subnet, and I’m allowing all hosts on that subnet to send jobs)

1
sudo distccd --log-file=/tmp/distccd.log --daemon -a 10.8.33.0/24

Setting up distcc (client) So, now you have to tell ccache to use the distcc distributed compilation servers. To do this, just add the following line to your ~/.bashrc file.
1
export CCACHE_PREFIX=distcc

Next, we have to tell distcc where to go to find the server. This also, just add to the bottom of your ~/.bashrc (my desktop is at 10.8.33.182 and it has 8 cores, my laptop is at localhost and has 4)
1
export DISTCC_HOSTS='localhost/4 10.8.33.182/8'


在另一个终端,使用下面命令检验ccache的运行效果

1
watch -n1 'ccache -s -v'

或者watch -n1 'ccache --show-stats -v'

编译时,使用top看 distcc process starts spooling up its threads.


Linux程序运行时被killed

linux程序运行时突然出现Killed,可以使用dmesg | egrep -i -B100 'killed process 查看原因,输出最近killed的信息。

参考: linux 程序被Killed,查看原因


有用的程序 1
  • 不断判断随机数的大小,如果它能大于某个值维持一段时间,输出时间,否则重新计时。
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
int main()
{
int num = 0;
int count = 0;
std::chrono::steady_clock::time_point start, end;
double duration;

while(1)
{
std::srand(time(0));
count = rand() % 1000000;
if(count > 680000)
{
if(num==0)
start = std::chrono::steady_clock::now();

num++;
end = std::chrono::steady_clock::now();
duration = std::chrono::duration<double>(end - start).count();
cout << "count: " << count << endl;
cout << "time elapsed " << static_cast<int>(duration) << endl;
}
else
{
duration = 0;
num = 0;
cout << endl;
}
}
return 0;
}
  • 判断一个点是否在多边形中

opencv函数: double pointPolygonTest(InputArray contour, Point2f pt, bool measureDist)

measureDist设置为true时,返回实际距离值。若返回值为正,表示点在多边形内部,返回值为负,表示在多边形外部,返回值为0,表示在多边形上。
当measureDist设置为false时,返回 -1、0、1三个固定值。若返回值为+1,表示点在多边形内部,返回值为-1,表示在多边形外部,返回值为0,表示在多边形上。

  • 计算点到直线的距离
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
* @tparam T point type
* @param first Given the starting point of a line segment
* @param last The endpoint of a given line segment
* @param third Given point
* @return T Distance from point to line
*/
template <typename T>
static T PointToLineDistance(const Point<T>& first, const Point<T>& last, const Point<T>& third) {
float dis_suqare =
((first.y - last.y) * third.x + (last.x - first.x) * third.y +
(first.x * last.y - last.x * first.y)) *
((first.y - last.y) * third.x + (last.x - first.x) * third.y +
(first.x * last.y - last.x * first.y)) /
((last.x - first.x) * (last.x - first.x) + (last.y - first.y) * (last.y - first.y));
return std::sqrt(dis_suqare);
}
  • 求图形的外接圆

OpenCV绘制最小外接矩形、最小外接圆
pointPolygonTest函数


Gazebo问题累计

当不正确关闭Gazebo时,再次启动Gazebo会遇到sever无法启动的问题。

1
Exception [Master.cc:50] Unable to start server[bind: Address already in use]. There is probably another Gazebo process running.

解决方法:用ps命令查找gzserver进程,将其kill。或是干脆重启系统


报错: Unable to convert from SDF version 1.7 to 1.6
把world文件的第一行改成 <sdf version = '1.6'>



文件使用了OBJ meshes,这是从Gazebo 7.4.0才可支持,使用gazebo -v发现我的gazebo版本是 7.0


报错 [joint_state_publisher-3] process has died 或者 UnicodeEncodeError: ‘ascii’ codec can’t encode characters in position xxx ordinal

去掉urdf、xacro、launch文件中的中文注释,或者改为英文,而且第一行不能有中文。


1
2
3
4
<plugin name='front_right_middle_sonar_sensor' filename='libgazebo_ros_range.so'>
<topicName>/prius/front_sonar/right_middle_range</topicName>
<frameName>front_right_middle_sonar_link</frameName>
</plugin>

.urdf.xacro文件中的颜色不识别。需要在文件开头定义颜色值,否则默认不识别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<material name="blue">
<color rgba="0.0 0.0 0.8 1.0"/>
</material>

<material name="red">
<color rgba="0.8 0.0 0.0 1.0"/>
</material>

<material name="white">
<color rgba="1.0 1.0 1.0 1.0"/>
</material>

<material name="yellow">
<color rgba="0.8 0.8 0.0 1.0"/>
</material>


运行Gazebo竟然出现错误,Gazebo页面卡住黑屏,在终端出现 Gazebo [Err] [REST.cc:205] Error in REST request

解决方案sudo gedit ~/.ignition/fuel/config.yaml,然后将 url : https://api.ignitionfuel.org注释掉,添加 url: https://api.ignitionrobotics.org


在Gazebo中进行小车仿真的过程中会出现如下警告 The root link base_link has an inertia specified in the URDF, but KDL does not support a root link with an inertia. As a workaround, you can add an extra dummy link to your URDF 该警告的意思是根关节的base_link在urdf中具有惯性参数,但是KDL不支持,建议的解决办法是,增加一个额外的dummy link

这个问题看似很麻烦,实际解决起来是非常简单的,只需添加一个link和一个joint即可:

1
2
3
4
5
6
<link name="dummy">
</link>
<joint name="dummy_joint" type="fixed">
<parent link="dummy"/>
<child link="base_link"/>
</joint>

当然link的名称是可以自己改的


gazebo版本升级以及环境太暗的解决方法


ubuntu18.04 装的Gazebo版本是9,升级方式,但是能不升级就不升级。


使用Gazebo时出现下面报错:

1
2
[spawn_entity.py-5] [ERROR] [1715655287.539237215] [spawn_entity]: Service %s/spawn_entity unavailable. Was Gazebo started with GazeboRosFactory?
[spawn_entity.py-5] [ERROR] [1715655287.540067060] [spawn_entity]: Spawn service failed. Exiting.

1
[ERROR] [gzserver-1]: process has died [pid 239512, exit code 255, cmd 'gzserver -slibgazebo_ros_init.so -slibgazebo_ros_factory.so -slibgazebo_ros_force_system.so'].

这是因为关闭的过程中有些Gazebo的程序没有完全关闭,导致再次启动时,无法正常开启。所以,只要把gazebo的相关程序关闭即可: killall gzserver。 此时,再次启动gazebo,就能正常打开了。

如果担心是自己程序的问题,使用ros2 launch gazebo_ros spawn_entity_demo.launch.py验证是不是Gazebo自身的问题


joint_state_publisher报错 UnicodeEncodeError: ‘ascii’ codec can’t encode characters in position 30-36: ordinal not in range(128)

  1. 可能是urdf文件里有中文注释,将其去掉
  2. 如果没有注释,修改两个文件 /opt/ros/melodic/lib/joint_state_publisher_gui/joint_state_publisher/opt/ros/melodic/lib/joint_state_publisher,在开头import sys之后增加
    1
    2
    reload(sys)  
    sys.setdefaultencoding('utf8')

或者修改文件/opt/ros/melodic/lib/python2.7/dist-packages/joint_state_publisher/__init__.py,也是添加

1
2
reload(sys) 
sys.setdefaultencoding("utf-8")

运行spawn_model也可能会同样的报错,到/opt/ros/melodic/lib/python2.7/dist-packages文件夹,然后新建一个sitecustomize.py文件,内容为

1
2
3
4
#coding=utf8
import sys
reload(sys)
sys.setdefaultencoding('utf8')

保存后给文件权限。


Git分支相关的命令
工具 gitk 也可以查看和管理分支
  • origin 远程服务器
  • origin/master 远程分支
  • master 本地分支

origin并不特别,就像分支名 master 在git中没有任何特殊意义一样.当执行git init时,master会作为初始分支的默认名字,因此使得master分支名被广泛使用。而origin是执行git clone时的默认服务器名称,当然可以通过指令git clone -o cat,使得默认服务器名称为cat,而默认远程分支为cat/master

从远程分支check out一个本地分支,该本地分支被称为追踪分支(tracking branch),被追踪的分支被称为上游分支(upstream branch),追踪分支可以理解为是和远程分支有直接关联的本地分支. 如果我们在追踪分支时执行git pull,git会自动知道需要获取和merge的分支的服务器.

git init之后,出现下面结果

1
2
3
4
5
6
7
8
9
提示:使用 'master' 作为初始分支的名称。这个默认分支名称可能会更改。要在新仓库中
提示:配置使用初始分支名,并消除这条警告,请执行:
提示:
提示: git config --global init.defaultBranch <名称>
提示:
提示:除了 'master' 之外,通常选定的名字有 'main''trunk''development'
提示:可以通过以下命令重命名刚创建的分支:
提示:
提示: git branch -m <name>

可以对分支改名。此时使用git branch无法查看分支,因为还没有任何操作。

拉取远程指定分支

1
2
git remote add origin project_url
git pull origin branch_name

在本地创建分支develop并切换到该分支

1
git checkout -b develop(本地分支名称)  origin/develop (远程分支名称)

远程分支上的内容都拉取到本地

1
2
# develop 为远程分支名称
git pull origin develop (远程分支名称)

回到本地文件夹查看,已完成拉取远程某个分支到本地。拉去更新也是这个命令,如果是git pull,可能会把所有分支的内容都拉下来,用时较长,没有必要。

提交到某分支

1
git push origin <本地分支名>:<远程分支名>

查看分支和合并

git branch -a list both remote-tracking and local branches

git branch -vv 会列出所有的分支信息,包含追踪分支的关联关系

常见的情况是,我自己有一个分支me,还要拉取另一个人的分支,两人共用部分文件,合并到分支master。在自己分支修改文件结束后,提交到自己的分支。然后切换到master分支,git checkout master。由于不知道另一个人是否更新了代码,所以先执行git pull更新一下,然后git merge me,这就合并了我的分支,再git push origin master

有时会出现冲突 ,这是由于两人修改了同一个文件,冲突的提示
1
2
3
Auto-merging readme.txt
CONFLICT (content): Merge conflict in readme.txt
Automatic merge failed; fix conflicts and then commit the result.
`git status` 也可以告诉我们冲突的文件. 在冲突的文件里,Git用<<<<<<<,=======,>>>>>>>标记出不同分支的内容,只能自己手动修改,然后再提交。解决冲突的过程一直是在`master`分支。 用带参数的`git log`也可以看到分支的合并情况 ## 将user分支合并到master分支
1
2
3
4
5
6
7
8
9
# 当前是user分支
git add test.yaml
git commit -m ":wrench:"
git push origin user
# 切换到 master 分支
git checkout master
git merge user
git push origin master
git checkout user
## 删除本地分支 git branch -d user
1
2
3
4
5
6
user@user:~/dev_ws/$ git branch -d user
error: The branch 'user' is not fully merged.
If you are sure you want to delete it, run 'git branch -D user'.

user@user:~/dev_ws/$ git branch -D user
Deleted branch user (was cf64937b)
## 删除远程分支, 千万小心使用 !
1
git push origin --delete remoteBranchName
## submodule 一个工程包含一个module,两部分的分支可能是不同的。先把工程拉取下来后,看看隐藏文件`.gitmodules`中的url和分支是否正确,此时的module部分应该还没有更新,使用下面的命令更新
1
2
git submodule init
git submodule update
## 本地文件修改后,执行git pull出现冲突 执行`git pull`出现下面提示
1
2
Please commit your changes or stash them before you merge.
Aborting
这是因为本地修改了一些文件,但远程仓库已经有别人提交更新了,所以有冲突。一般不会直接commit本地的修改。 1. 放弃本地修改的改法 (这种方法会丢弃本地修改的代码,而且不可找回!)
1
2
git reset --hard
git pull
## 将最新的主分支更新到个人分支
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 当前在个人分支
git add . # 将本地修改文件加入暂存区
git commit -m "修改内容" # 提交日志

git checkout master # 切换到主分支(建议操作到这里的时候利用git branch
git pull # 将本地主分支代码更新
git checkout self-branch # 切换到自分支

git merge master # 将主分支代码合并更新到自己分支

git push # 提交到自分支远程端

git checkout master # 切换到主分支

git merge self-branch # 将自己代码合并更新到本地主分支master

git push # 将本地代码推到远程主分支master上
## 难题 1
1
2
3
4
5
6
7
# 执行 git status 的结果
On branch master
Your branch and 'origin/master' have diverged,
and have 5 and 21 different commits each, respectively.
(use "git pull" to merge the remote branch into yours)

nothing to commit, working tree clean
这发生在多人可能操作 master 分支的时候,我自己的分支提交完了,切换到master分支,要执行合并时,出现这个问题,并不是文件冲突的问题。按如下方法解决: 如果不需要保留本地的修改,只要执行下面两步:
1
2
git fetch origin
git reset --hard origin/master
当我们在本地提交到远程仓库的时候,如果遇到上述问题,我们可以首先使用如下命令:
1
2
3
4
git rebase origin/master
git pull --rebase
# 提交到远程仓库
git push origin master
## 一次合并冲突的解决 我在自己的分支`me`开发,主分支比我领先很多。有一次,我从git上下载了主分支的压缩包,然后手动覆盖了一部分文件以更新,这其实是不正确的操作。 第二天我先切换到主分支,更新到最新,然后切换到`me`分支,提交到远程后,合并主分支,结果出现的冲突比我自己修改的多很多,因为把我昨天手动覆盖的那些也包括了。这种情况下,只能手动解决冲突,但是大部分文件一定是用主分支,如果一个个打开会很麻烦。 用VSCode打开工程,分支管理里会显示文件冲突,对使用主分支版本的文件,右键选择就可以,而且能同时选多个文件。解决完之后,选择暂存文件,之后可以push了。
fatal: 无法访问 'http://192.168.9.61/intelligentcleaningrobotbu/j36/algorithm/planning_control.git/':The requested URL returned error: 502

需要关闭代理软件


auto和decltype

声明一个变量为 auto 类型时, 编译器将自动帮助你推导出合适的数据类型. 这个变量可以是 :

  • 声明后立即赋值的普通变量;
  • 函数的返回值;
  • 函数的形参 (C++14 起)

当你需要某个表达式的返回值类型而又不想实际执行它时用decltype

1
2
3
4
5
6
7
int  a=8, b=3;

//运行时需要实际执行a+b,哪怕编译时就能推导出类型
auto c = a+b;

//编译期类型推导不可以用auto c; 直接声明变量,必须同时初始化。
decltype(a+b) d;

decltype 的推导规则遵循如下几点 :

  • 若表达式是一个 不带括号的标记符表达式 或 类/结构体成员访问表达式, 那么推导的结果是所代表实体的类型;
  • 若表达式是一个函数调用(包括操作符重载), 那么推导的结果是函数的返回类型, 若返回值是基础类型则抛弃 const 限定符;
  • 若表达式是一个字符串字面量, 则推到为 const 左值引用;
  • 上述情况以外, 若表达式结果为左值则推导为左值引用, 否则推导为本类型;