异常处理/__LINE__ 与 __FILE__ 宏在调试和异常处理中的高级使用

文章目录

  • 概述
  • 痛点分析
  • _LINE_ 代码所在行号
    • _LINE_ 直接转为字符串
    • _LINE_ 作为整型数据使用
    • _LINE_标记宏函数的调用位置
  • _FILE_ 代码所在文件名
    • 简单实验
    • 不期望 _FILE_ 宏代表全路径
  • assert 使用了 _FILE_ 和 _LINE_
  • 借助TLS技术
  • 小结

概述

_LINE_和_FILE_是C/C++中的预定义宏,分别用于获取当前代码所在的行号和文件名。本文详细讲述了它们的使用场景和使用方法,消除了一些陈旧错误的理解,重点实践了它们在软件调试、系统异常处理过程中举足轻重的作用。
编译器在预处理阶段将它们替换为相应的值,具体来说:_LINE_宏会被替换为当前代码所在的行号,表示该行号在源文件中的位置;_FILE_宏会被替换为当前代码所在的文件名,表示包含该代码的源文件的文件名。通过在日志记录或错误消息中使用_LINE_宏,可以动态地获取代码中出现问题的位置,并将其包含在日志或错误消息中,以帮助开发人员在调试和错误处理中排查问题。要再次强调的是,这些宏的值是在编译时确定的,因此它们提供的行号和文件名是编译时的信息,而不是运行时的信息。

转载请标明原文链接,
https://blog.csdn.net/quguanxin/category_6223029.html

痛点分析

#define STRINGIFY(x) #x          //it won't take effect, if you just do this
#define TOSTRING(x) STRINGIFY(x) //it works
//
std::string generateErrorMessage(std::string msg) {
    return msg + " in " + __FILE__ + " at line " + /*std::to_string(__LINE__)*/ TOSTRING(__LINE__);
}
//
int main() {
    std::string errorMsg = generateErrorMessage("Something went wrong.");
    std::cout << errorMsg.c_str() << std::endl;
    system("pause"); return 0;
}

执行结果,
在这里插入图片描述
如上,我们期望标记的是第13行代码位置,而运行结果却只能是 LINE 出现的第9行代码。FILE 存在相似的情况。

LINE 代码所在行号

早些年由于对_LINE_宏的不够了解,以为,既然它是编译时确定的,那么就只能死板的在待调试行上写包含_LINE_宏的代码,而不能封装或传递它。而实际上,配合宏函数的使用或者将其作为size_t 类型的函数实参,可以非常灵活的使用它。_LINE_宏可以直接转为字符串、可以作为int类型使用、也可以封装在宏函数内部(最终客户端调用宏函数,LINE 被转换为宏函数所在的行),接下来我们展开论证。

LINE 直接转为字符串

#include <iostream>
#include <string>

#define STRINGIFY(x) #x          //it won't take effect, if you just do this
#define TOSTRING(x) STRINGIFY(x) //it works

// 定义一个宏来生成带有错误消息、文件名和行号的字符串
#define ERROR_MSG(msg) ("Error: " + std::string(msg) + " in " + __FILE__ + " at line " + TOSTRING(__LINE__))

int main() {
    std::string errorMsg = ERROR_MSG("Something went wrong.");
    std::cout << errorMsg << std::endl;
    return 0;
}

在这里插入图片描述
在这里插入图片描述
(操作符#)是C/C++中的预处理器操作符,称为字符串化操作符(stringify operator)。在预处理阶段,它可以将宏参数或标识符转换为字符串常量。具体来说,#操作符会在宏展开过程中将其后面的标识符或参数转换为一个以双引号括起来的字符串。

LINE 作为整型数据使用

我们以 ros2/rcutils/error_handling.h中的函数为例,

/// Set the error message, as well as the file and line on which it occurred.
/**
 * This is not meant to be used directly, but instead via the
 * RCUTILS_SET_ERROR_MSG(msg) macro.
 *
 * The error_msg parameter is copied into the internal error storage and must
 * be null terminated.
 * The file parameter is copied into the internal error storage and must
 * be null terminated.
 *
 * \param[in] error_string The error message to set.
 * \param[in] file The path to the file in which the error occurred.
 * \param[in] line_number The line number on which the error occurred.
 */
RCUTILS_PUBLIC void rcutils_set_error_state(const char * error_string, const char * file, size_t line_number);

/// Set the error message, as well as append the current file and line number.
/**
 * If an error message was previously set, and rcutils_reset_error() was not called
 * afterwards, and this library was built with RCUTILS_REPORT_ERROR_HANDLING_ERRORS
 * turned on, then the previously set error message will be printed to stderr.
 * Error state storage is thread local and so all error related functions are
 * also thread local.
 *
 * \param[in] msg The error message to be set.
 */
#define RCUTILS_SET_ERROR_MSG(msg) \
  do {rcutils_set_error_state(msg, __FILE__, __LINE__);} while (0)

如上,首先在编译预处理阶段,LINE 被替换为 RCUTILS_SET_ERROR_MSG 宏函数的代码行号,然后其作为一个整型数据,也即作为 rcutils_set_error_state 函数 line_number 参数的实参被传递。接下来我们定义一个简单的可接收 _FILE_, __LINE__实参的函数,

//形参类型分别对应 const char* 和 size_t 
std::string generateErrorMessage(std::string msg, const char* file, size_t line_no) {
    return msg + " in " + file + " at line " + std::to_string(line_no);
}
//
int main() {
    std::string errorMsg = generateErrorMessage("Something went wrong.", __FILE__, __LINE__);
    std::cout << errorMsg << std::endl;
    system("pause");  return 0;
}

在这里插入图片描述
执行结果如上,LINE 被识别为其出现位置所在的行号。这种方案很好理解,但在实际使用中每次都要去传递 FILELINE 数据,让人感觉不是很舒服。一种更高级的办法是,将上述 generateErrorMessage 用宏函数封装。具体我们看下一小节。

_LINE_标记宏函数的调用位置

一个应对策略就是,定义宏函数,

//
#define ERROR_MSG(errorMsg, msg) \
{ \
    errorMsg = generateErrorMessage("Error:" + std::string(msg), __FILE__, __LINE__); \
}  \
//一种更优雅的写法是
#define ERROR_MSG(errorMsg, msg) \
do { errorMsg = generateErrorMessage("Error:" + std::string(msg), __FILE__, __LINE__); } while (0) 

C/C++语法要求在宏展开时,宏展开的结果必须是一个完整的语句。故在使用宏定义时,通常使用do {…} while(0)的技巧可以确保宏的语法完整性,使其在被展开时能够像代码块一样使用,且可以避免语法错误,提高代码可读性。当然,你也可以仅使用花括号。
在这里插入图片描述
测试结果如下,
在这里插入图片描述
如上测试结果表明,ERROR_MSG函数中无论_LINE_出现在其中的第几行,都无关紧要,_LINE_标记的是 ERROR_MSG 宏的调用位置(如上图行号为27行),而不是 _LINE_标记的直接位置 (如上图行号22行)。至此,算是消除了对 _LINE_的一个大误会,它的使用方法,远比我之前以为的要灵活。

FILE 代码所在文件名

在前文讲述 LINE 宏的过程中,也同时完成了 FILE 宏的使用实践,它是一个字符串常量,表示当前源文件的文件名,包括文件的路径,其对应的数据类型是 const char* ,也即 C 语言字符串。

简单实验

#include <stdio.h>
#include <iostream>
//
int main() {
    printf("当前源文件名:%s\n", __FILE__);
    system("pause");  return 0;
}

在这里插入图片描述
如上,输出 FILE 所在的代码文件的全路径。当资源很紧张,或者文件路径较深的时候,全路径名就会很烦人,咋办?

不期望 FILE 宏代表全路径

如上一小节,在Windows上使用 _FILE_ 宏,默认情况下其代表的是源代码文件的全路径名称,但在大多情况下,这会显得有点冗余、浪费资源。一般情况在同一个项目下,存在同名文件的可能性不大,同名且内容相同的可能更是不存在,因此我们仅保留文件名就可以。为此我们对 _FILE_ 宏进行如下重定义,

#ifdef _WIN32
#define FILE_NAME (strrchr(__FILE__, '\\') ? strrchr(__FILE__, '\\') + 1 : __FILE__)
#else
#define FILE_NAME (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__)
#endif

定义一个名为FILE_NAME的宏,它使用strrchr函数来查找最后一个斜杠字符’/',并返回该字符后面的字符串部分。如果没有斜杠字符,则直接返回__FILE__宏的值。要注意的是,在不同操作系统上,文件路径使用不同的分隔符。对比效果如下,清澈了许多,
在这里插入图片描述
测试用的源代码如下,

#include <stdio.h>
#include <iostream>
//
#ifdef _WIN32
#define FILE_NAME (strrchr(__FILE__, '\\') ? strrchr(__FILE__, '\\') + 1 : __FILE__)
#else
#define FILE_NAME (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__)
#endif
//
int main() {
    printf("当前源文件名:%s\n", FILE_NAME);
    system("pause"); return 0;
}

assert 使用了 FILELINE

int main() {
    printf("当前源文件名:%s, 当前代码行号:%s\n", FILE_NAME, __LINE__);
    assert(0 == 1);
    system("pause"); return 0;
}

在这里插入图片描述
通过上述测试,可以猜测,assert 内部极有可能是封装了 FILELINE 预处理宏的。看一下源码,

#ifdef NDEBUG
    #define assert(expression) ((void)0)
#else
    _ACRTIMP void __cdecl _wassert(
        _In_z_ wchar_t const* _Message,
        _In_z_ wchar_t const* _File,
        _In_   unsigned       _Line
        );

    #define assert(expression) (void)(                                                       \
            (!!(expression)) ||                                                              \
            (_wassert(_CRT_WIDE(#expression), _CRT_WIDE(__FILE__), (unsigned)(__LINE__)), 0) \
        )
#endif

与我们前面自定义函数的使用方法一致,先以 char* 和 int 为参数类型,定义函数,然后,使用宏封装此函数,这样,LINEFILE 就可以用来代表宏函数(assert) 的调用位置。

借助TLS技术

参见 src/error_handling.c 中的实现,定义线程本地存储TLS变量,

// g_ is to global variable, as gtls_ is to global thread-local storage variable
RCUTILS_THREAD_LOCAL bool gtls_rcutils_thread_local_initialized = false;
RCUTILS_THREAD_LOCAL rcutils_error_state_t gtls_rcutils_error_state;
RCUTILS_THREAD_LOCAL bool gtls_rcutils_error_string_is_formatted = false;
RCUTILS_THREAD_LOCAL rcutils_error_string_t gtls_rcutils_error_string;
RCUTILS_THREAD_LOCAL bool gtls_rcutils_error_is_set = false;

到这里就有点偏了… 已经超出了这个小主题… 跑到错误处理中了…

小结

在《异常处理/分析ROS2异常处理的设计和实现思路》(尚未发布)一文中,有提到过,针对调试信息,越直接越好,而 LINEFILE 宏所表现出来的,几乎就是最直接的。
站在开发者的角度上,无论是何种形式的异常处理,都是手段,我们根本目的始终是快速定位程序运行过程中的问题,并尽力地使其从问题中恢复正常运行。不同于此的,用户角度,作为软件的使用者,用户希望看到的告警信息应该是,可读性强、及时性好、清晰明了、具体详细的,并且好的告警信息不仅指出问题,还应该提供解决方案或建议,是可以操作和控制的。用户绝对不希望看到晦涩难懂的告警信息,而是希望能够快速地理解问题所在,因此给用户的告警信息必须是能简练和准确描述问题本质和原因的。虽说是具体详细,但也绝不是详细到哪个代码行,这是很容易理解的。用户期望了解的问题的具体细节,应该是业务层级的,操作层级的,而不是系统实现层级。

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

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

相关文章

【Sql-02】 求每个省份最新登陆的三条数据

SQL 输出要求数据准备sql查询结果 输出要求 要求输出&#xff0c;userid_1,logtime_1,userid_2,logtime_2,userid_3,logtime_3 数据准备 CREATE TABLE sqltest (province varchar(32) NOT NULL,userid varchar(250) DEFAULT NULL,logtime datetime ) ENGINEInnoDB DEFAULT C…

Spring框架中常见注解

Spring&#xff1a; SpringMVC&#xff1a; RequestMapping用在类上表示所有该类下方法的父路径 RequestParam 做映射&#xff0c;前端请求的参数映射到控制器Controller的处理方法上的参数上。 【当参数需要设置默认值&#xff08;前端没有发送这个参数&#xff09;、参数名…

禁止打开浏览器时弹出 internet explorer 11 停用的通知

计算机管理&#xff08;我的电脑图标上右键&#xff09; - 管理模板 - windows 组件 - internet explorer 启用隐藏 internet explorer 11 停用通知&#xff0c;如下图所示

每日算法之二叉树的最近公共祖先

题目描述 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 百度百科中最近公共祖先的定义为&#xff1a;“对于有根树 T 的两个节点 p、q&#xff0c;最近公共祖先表示为一个节点 x&#xff0c;满足 x 是 p、q 的祖先且 x 的深度尽可能大&#xff08;一个节点也可以是…

【Python特征工程系列】排列重要性法分析特征重要性-随机森林模型为例(案例+源码)

这是我的第277篇原创文章。 一、引言 排列重要性&#xff08;Permutation Importance&#xff09;是一种基于模型的方法&#xff0c;用于评估每个特征对模型性能的影响程度。该方法通过随机打乱单个特征的值并观察模型性能的变化&#xff0c;从而确定特征的重要性。如果某个特征…

模型预测控制与模糊控制 —— 潜力控制方案探讨

一、需要多少先验信息&#xff1f; 此图片来源于网络&#xff0c;所有的控制与估计过程都涉及了先验信息与后验信息之间的博弈 评估一个控制方案对先验信息的需求量大小和先验信息质量对其影响的方法涉及以下几个方面&#xff1a; 1、控制方案的理论分析&#xff1a; 详细分析…

【UE Niagara】在UI上生成粒子

效果 步骤 1. 在虚幻商城中将“Niagara UI Render”插件安装到引擎 2. 打开虚幻编辑器&#xff0c;勾选插件“Niagara UI Renderer”&#xff0c;然后重启编辑器 3. 先创建一个控件蓝图&#xff0c;该控件蓝图只包含一个按钮 这里设置尺寸框尺寸为200*50 4. 显示该控件 5. 新…

MFC中关于CMutex类的学习

MFC中关于CMutex类的学习 最近在项目中要实现两个线程之间的同步&#xff0c;MFC中提供了4个类&#xff0c;分别是CMutex(互斥量)、CCriticalSection(临界区)、CEvent(事件对象)、CSemaphore(信号量)。有关这4个类的说明&#xff0c;大家可以参考微软官方文档&#xff1a; CM…

4. 从感知机到神经网络

目录 1. 从感知机到神经网络 2. 最简单的神经网络 3. 激活函数的引入 1. 从感知机到神经网络 之前章节我们了解了感知机&#xff0c;感知机可以处理与门、非与门、或门、异或门等逻辑运算&#xff1b;不过在感知机中设定权重的工作是由人工来做的&#xff0c;而设定合适的&a…

AI算法工程师课程学习-数学基础-高数1-微积分

机器学习数学基础学习路线&#xff1a;1.高中数学-->大学2.微积分-->3.线性代数-->4.概率论-->5.优化理论。 为尽快进入到AI算法课程的学习&#xff0c;现在高数的学习要求&#xff1a; 1.看得懂&#xff0c;知道是什么&#xff0c;能听得懂&#xff0c;能理解讲…

Java框架精品项目【用于个人学习】

源码获取&#xff1a;私聊回复【项目关键字】获取 更多选题参考&#xff1a; Java练手项目 & 个人学习等选题参考 推荐菜鸟教程Java学习、Javatpoint学习 前言 大家好&#xff0c;我是二哈喇子&#xff0c;此博文整理了各种项目需求 此文下的项目用于博主自己学习&#x…

文本三剑客grep与正则表达式、元字符

正则表达式 正则表达式又称为正规表达式、常规表达式、在代码中常简写为regex、regex或RE。正则表达式是使用单个字符串来描述、匹配一系列符合某个句法规则的字符串&#xff0c;简单来说&#xff0c;是一种匹配字符串的方法&#xff0c;通过一些特殊符号&#xff0c;实现快速查…

无人播剧直播收益在哪里!快手无人播剧新秘籍:版权无忧,日入四位数攻略

无人播剧顾名思义就是通过短视频平台直播不需要真人出镜受众群体通过网络短视频平台看到的经典影视剧集可以实现24小时不停断的播放利用多种途径变现的一种直播形式 1、操作简单、不露脸、不出镜2、手机、电脑都可以操作3、可以矩阵操作4、0粉丝、0作品、0保证金就可以开播5、…

FreeRTOS任务调度器

目录 1、什么是任务调度器 2、FreeRTOS中的任务调度器 2.1 抢占式调度 2.2 时间片调度 2.3 协作式调度 3、任务调度案例分析 3.1 实验需求 3.2 CubeMX配置 3.3 代码实现 3.3.1 uart.c 重定向printf 3.3.2 打开freertos.c并添加代码 3.3.4 代码现象 1、什么是任务调度…

家装新宠!装修APP开发解决方案,为业主提供全新装修模式

随着人们对家庭装修的需求度越来越高&#xff0c;装修APP开发也随之出现。如今装修APP开发可实现互联网与传统家装行业的信息结合&#xff0c;由传统的家装行业广告模式向移动端的互联网模式进行转移&#xff0c;实现传统家装行业与互联网的相辅相成&#xff0c;以此来推动家装…

Maven 插件使用

1.spring-boot-maven-plugin 我们直接使用 maven package &#xff08;maven自带的package打包功能&#xff09;&#xff0c;打包Jar包的时候&#xff0c;不会将该项目所依赖的Jar包一起打进去&#xff0c;在使用java -jar命令启动项目时会报错&#xff0c;项目无法正常启动。…

每日一题8:Pandas-改变数据类型

一、每日一题 编写一个解决方案来纠正以下错误&#xff1a; grade 列被存储为浮点数&#xff0c;将它转换为整数。 返回结果格式如下示例所示。 解答&#xff1a; import pandas as pddef changeDatatype(students: pd.DataFrame) -> pd.DataFrame:students[grade] studen…

Java入门基础学习笔记7——Intellij IDEA开发工具概述、安装

之前的开发工具存在一些问题&#xff1a; 文本编辑工具&#xff1a;记事本、NotePad、EditPlus、Sublime...编写代码的时候没有错误提醒、没有智能代码提示、需要自己进行编译、执行、功能不够强大。 集成开发环境&#xff08;IDE&#xff1a;Integrated Development Environm…

推荐4个可用的github国内镜像

Github是全球最大的代码托管云平台&#xff0c;超过1亿用户在平台上分享代码及数据&#xff0c;深受生物信息学软件开发者的喜爱&#xff0c;并且现在发表文章&#xff0c;若涉及到代码&#xff0c;编辑还要求我们把代码及数据存放在github上&#xff0c;以便检查数据的真实性和…

matlab使用教程(68)—修改双y轴图的属性

使用 yyaxis 函数可创建一个左右两侧都有 y 轴的 Axes 对象。与 y 轴有关的坐标区属性有两个值。但是&#xff0c;MATLAB 仅允许访问活动侧的值。例如&#xff0c;如果左侧处于活动状态&#xff0c;则 Axes 对象的 YDir 属性包含左侧 y 轴的方向。同样&#xff0c;如果右侧处于…