2021SC@SDUSC
这一篇分析体会是在看imageProjection.cpp的时候产生的
每个cpp文件里面的节点初始化的节点名称一样,与节点名称唯一矛盾,为什么?
现象如下:
这是imageProjection.cpp的main函数:
int main(int argc, char** argv)
{
ros::init(argc, argv, "lidar");
ImageProjection IP;
ROS_INFO("\033[1;32m----> Lidar Cloud Deskew Started.\033[0m");
ros::MultiThreadedSpinner spinner(3);
spinner.spin();
return 0;
}
这是featureExtraction.cpp的main函数:
int main(int argc, char** argv)
{
ros::init(argc, argv, "lidar");
FeatureExtraction FE;
ROS_INFO("\033[1;32m----> Lidar Feature Extraction Started.\033[0m");
ros::spin();
return 0;
}
经过对比,可以发现它们的节点初始化函数传进去的节点名称是一样的都是"lidar"
,而且,这两个文件是同时运行的。而且,在运行的时候,使用rqt_graph
打印节点时,也没有出现lidar
。于是带着这个疑问,我仔细地思考整个ros运行地流程,并且查阅网上的资料。我发现,网上虽然也是用ros::init
进行节点初始化的,参数也包含了节点的名称,但是,在运行编译后的节点使用rosrun
启动的,因此,用rqt_graph打印出来的就是ros::init
里面的名字参数。
而我们使用lvi-sam是直接用roslaunch去启动节点,而在比赛的时候,我也稍微学习了roslaunch的用法,里面是有自己定义节点的各个属性,其中包括了名称。因此,我找到了启动lidar_odometry里面4个cpp文件对应的launch文件,找到了答案。
<launch>
<arg name="project" default="lvi_sam"/>
<rosparam file="$(find lvi_sam)/config/params_lidar.yaml" command="load" />
<param name="vins_config_file" type="string" value="$(find lvi_sam)/config/params_camera.yaml" />
<node pkg="$(arg project)" type="$(arg project)_imuPreintegration" name="$(arg project)_imuPreintegration" output="screen" respawn="true"/>
<node pkg="$(arg project)" type="$(arg project)_imageProjection" name="$(arg project)_imageProjection" output="screen" respawn="true"/>
<node pkg="$(arg project)" type="$(arg project)_featureExtraction" name="$(arg project)_featureExtraction" output="screen" respawn="true"/>
<node pkg="$(arg project)" type="$(arg project)_mapOptmization" name="$(arg project)_mapOptmization" output="screen" respawn="true"/>
<node pkg="$(arg project)" type="$(arg project)_visual_feature" name="$(arg project)_visual_feature" output="screen" respawn="true"/>
<node pkg="$(arg project)" type="$(arg project)_visual_odometry" name="$(arg project)_visual_odometry" output="screen" respawn="true"/>
<node pkg="$(arg project)" type="$(arg project)_visual_loop" name="$(arg project)_visual_loop" output="screen" respawn="true"/>
<node pkg="image_transport" type="republish" name="$(arg project)_republish" args="compressed in:=/camera/image_raw raw out:=/camera/image_raw" output="screen" respawn="true"/>
launch>
可以看到,launch文件中,已经定义好了节点的名称,而这个名称,在rqt_graph
也能找到。不过,在这里,我们可以又多问几个为什么。
ros::init
还需要吗?或者,还需要main函数吗? 为了搞清楚,roslaunch和rosrun有什么不同,我翻阅了一下wiki。roslaunch允许在本地和远程启动多个节点,并且可以在参数服务器上设置参数。而rosrun就是很简单的允许你运行一个可执行文件。显然,是rosrun运行是直接运行可执行文件,会直接从main函数进入,然后就执行了init函数,其中的名称参数也就成了节点的名称;而roslaunch应该也和rosrun在一开始运行节点是一样的,也是从main函数进入,但是,其中设置参数这个步骤会把init时设置的名称参数覆盖,因此,只要launch文件中的节点参数设置不重复,用roslaunch启动的节点名称也就不会重复。
至于第二个问题,按照我上面的推论,应该还是需要ros::init
以及main函数的,毕竟这是让系统运行代码,创建进程的一个入口(笑+摊手)。
在一开始看LVI-SAM的源代码的时候,我被C++的第一个语法就迷惑了:
subImu = nh.subscribe<sensor_msgs::Imu>(imuTopic, 2000, &ImageProjection::imuHandler, this, ros::TransportHints().tcpNoDelay());
这里的函数名后面跟着一个尖括号。按照我对这个语句的理解,我可以推测这里尖括号里面的应该是消息的内容,但是为什么这里的语法会这样写呢?
于是,我去ros官网,找了ros::NodeHandle
这个类的源代码,subscribe这个函数有很多重载的版本,下面是上面的代码的参数对应的版本:
template<class M, class T>
Subscriber subscribe(const std::string& topic, uint32_t queue_size, void(T::*fp)(M), T* obj,
const TransportHints& transport_hints = TransportHints())
{
SubscribeOptions ops;
ops.template initByFullCallbackType<M>(topic, queue_size, boost::bind(fp, obj, _1));
ops.transport_hints = transport_hints;
return subscribe(ops);
}
因此,我可以进一步推测,这个尖括号的写法应该是和模板函数有关,但是问题又来了,我们平常的模板函数的调用应该是和下面的代码一样的呀?
#include
using namespace std;
template<class T>
void Swap(T & x, T & y)
{
T tmp = x;
x = y;
y = tmp;
}
int main()
{
int n = 1, m = 2;
Swap(n, m); //编译器自动生成 void Swap (int &, int &)函数
double f = 1.2, g = 2.3;
Swap(f, g); //编译器自动生成 void Swap (double &, double &)函数
return 0;
}
在我查阅网上的资料后,我发现,上面的代码这样写:调用模板函数和调用普通的函数没区别,是因为传递参数的时候已经指定了模板变量T的类型,而如果没有指定类型的话,需要在函数调用的时候,函数名后面写上。我写了小例子如下,里面包含了上述的两种情况:
#include
using namespace std;
template<class M, class T> // 不知道M的类型,需要显示指定
void f(int a, T b) {
cout << "result is " << (a+b) << endl;
M c = 'a'; // char转int
cout << c << endl;
}
int main()
{
int a = 1;
f<int>(a, a); // 显式指定M的类型为int
}
在上面的例子中,我们可以看到这个ros的源代码的参数列表有个不太寻常的地方:void(T::*fp)(M)
这种参数的写法其实就是把函数当成参数进行传递。对于这个机制,我写稍微写了个代码帮助理解:
#include
using namespace std;
int f(int a, int b) {
return a+b;
}
int g(int a, int b, int(*t)(int x, int y)) {
return t(a, b);
}
int main()
{
printf("the result is %d;\n", g(1, 2, f));
}
运行结果如下:
我们可以稍微深入一点,加入我们写的函数参数的返回值不一样会怎么样?如下:
#include
using namespace std;
int f(int a, int b) {
return a+b;
}
int g(int a, int b, double(*t)(int x, int y)) {
return t(a, b);
}
int main()
{
printf("the result is %d;\n", g(1, 2, f));
}
可以看到,编译的时候会报错:
如果修改函数签名,也是如此,因此,如果函数要作为参数去传递的话,函数签名和返回值要完全相同才行。
这算是比较基础的知识,当c++里面用了宏的时候,可以不用使用分号';'
,因为这算是字符的替换。如下:
#define MAX A();
class A {
MAX
};
int main() {
A a;
return 0;
}