vs中自定义C++对象调试视图

vs中自定义C++对象调试视图

背景

VS中提供了自定义C++对象的调试视图的功能。

自定义C++对象的调试视图可以自定义在vs的调试视图中如何显示一个对象的状态和信息。

  • 当类的内部实现非常复杂时,直接通过调试信息中的成员变量无法直观的了解对象的状态,尤其是使用第三方开发的库时,更不可能了解每个成员变量的含义。如果提供自定义的调试视图就能够提供非常友好、高可读性的调试状态信息。
  • 当使用了某些方法将类的内部实现隐藏时时,如果导出的调试信息中不包含内部实现类,那么使用此类的开发者看不到此对象的任何调试信息。如果提供自定义的调试视图就能够提供必要的的调试状态信息,方便使用此类的开发者。

C++标准库中大部分的类都有复杂的内部实现,如果调试时直接从成员变量的值来进行观察的话,无法直观的得到对象的状态。VS为几乎C++标准库中所有的类都提供了自定义的调试视图,可以直观的让开发者得到C++标准库对象的状态。

image
image

调试视图

调试视图文件

vs是通过.natvis文件来自定义调试视图的,VS提供了STL.natvis文件,其中有C++标准库的调试视图。

调试视图描述语法

详细的调试视图描述语法请参考VS官方文档,本文只作简单介绍。

.natvis是一个xml文件,其格式大致如下:

<?xml version="1.0" encoding="utf-8"?>
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
  <Type Name="MyNamespace::CFoo">
    .
    .
  </Type>

  <Type Name="...">
    .
    .
  </Type>
</AutoVisualizer>

每个Type元素描述一个类的调试视图,Name属性描述类名(包含所有的命名空间)。

如果类是一个模板类,则可以在模版参数的位置使用*通配符来匹配任意模板参数。

Type中的典型结构如下:

<Type Name="MyNamespace::MyClass">
    <DisplayString>{{x={x} y={y}}}</DisplayString>
    <Expand>
      <Item Name="[State]">_M_State</Item>
      <Item Name="[Exception]" Optional="true">_M_exceptionHolder</Item>
    </Expand>
</Type>
  • DisplayString 描述一个对象的简要的信息,通常会描述对象最重要的一些信息。
  • Expand 描述对象要展开显示的成员信息
  • Item 描述对象展开显示的具体一个成员信息,Name属性描述显示在调试视图中的名称。

DisplayString元素的值是一个字符串,字符串中可以嵌入代码表达式,用{}括起来的部分会被当做代码表达式计算,其他部分直接作为字符串显示。连续两个大括号{{}}可以作为转义来表示大括号的字符串。

例如{{x={x} y={y}}}这个字符串最终在调试视图中显示的是{x=3 y=5}

Item元素的值是一个代码表达式,在调试视图中查看时直接被计算。

除了Item元素以外,还有一些显示不定长复杂信息的元素,例如TreeItems显示树形结构、ArrayItems 显示数组结构等。STL.natvis中有大量的例子。

DisplayStringItem元素都可以添加一些属性来在控制在某些条件下生效:

  • Condition 此属性的值是一个代码表达式,只有当代码表达式的结果是true时,此元素才会显示
  • Optional 此属性的值是true或者false,当为true时,如果此元素的表达式无法解析,则调试器会忽略该元素节点。否则表达式解析失败会导致整个调试视图加载失败。

未导出成员的调试视图描述

如果当类中的成员使用一些前置声明,隐藏了内部的成员,那么当运行堆栈不在此模块的内部时,此时代码表达式直接使用涉及到内部成员的表达式时无法生效的。

在dll的导出类中使用前置声明和impl模式来隐藏内部实现时经常会遇到此类问题,如果运行堆栈不在dll内部,那么调试窗口中无法查看dll导出的类对象隐藏的内部成员信息。

Item元素值的代码表达式在运算时使用的是当前的上下文,也就是说其显示的结果和将此表达式直接添加到变量查看窗口中是一致的,所以哪怕在调试视图描述中声明了信息,但是只要涉及到了类对象隐藏的内部成员,就无法解析。

这种情况下可以在调试视图中使用手动计算偏移和强制转型的办法来显示信息,C++标准库中有大量这种隐藏内部实现的类,我们可以参考一下:

<Type Name="std::mutex">
      <AlternativeType Name="std::recursive_mutex"/>
      <DisplayString Condition="sizeof(void *) == 4u &amp;&amp; *(int *)((char *)(&amp;_Mtx_storage) + 44) == 0">unlocked</DisplayString>
      <DisplayString Condition="sizeof(void *) == 4u &amp;&amp; *(int *)((char *)(&amp;_Mtx_storage) + 44) != 0">locked</DisplayString>
      <DisplayString Condition="sizeof(void *) == 8u &amp;&amp; *(int *)((char *)(&amp;_Mtx_storage) + 76) == 0">unlocked</DisplayString>
      <DisplayString Condition="sizeof(void *) == 8u &amp;&amp; *(int *)((char *)(&amp;_Mtx_storage) + 76) != 0">locked</DisplayString>
      <Expand>
          <Item Name="[locking_thread_id]" Condition="sizeof(void *) == 4u &amp;&amp; *(int *)((char *)(&amp;_Mtx_storage) + 44) != 0">*(long *)((char *)(&amp;_Mtx_storage) + 40)</Item>
          <Item Name="[locking_thread_id]" Condition="sizeof(void *) == 8u &amp;&amp; *(int *)((char *)(&amp;_Mtx_storage) + 76) != 0">*(long *)((char *)(&amp;_Mtx_storage) + 72)</Item>
          <Item Name="[ownership_levels]" Condition="sizeof(void *) == 4u &amp;&amp; *(int *)((char *)(&amp;_Mtx_storage) + 44) != 0">*(int *)((char *)(&amp;_Mtx_storage) + 44)</Item>
          <Item Name="[ownership_levels]" Condition="sizeof(void *) == 8u &amp;&amp; *(int *)((char *)(&amp;_Mtx_storage) + 76) != 0">*(int *)((char *)(&amp;_Mtx_storage) + 76)</Item>
      </Expand>
  </Type>

可以看到,VS直接将位置类型的内部成员指针进行了偏移后强制进行了转型,同时通过Condition属性兼容了64位和32为的情况。

所以,当遇到隐藏内部实现类时,可以在调试视图中的表达式中对this指针手动计算成员的偏移并使用强制转型来计算成员的值。

强制转型的调试视图加载

VS的调试视图会在遇到对应类型变量的时候加载,这就带来一个问题,如果当前的代码中没有使用到一个的类型,那么在调试窗口中计算一个包含此类型的表达式时就无法加载对应类型的调试视图。

比如,在调试第一个空的程序时调试窗口中填写如下一个表达式:(std::shared_ptr<int>*)(nullptr)。那么调试窗口会提示std::shared_ptr<int>未定义。

image

但是当你代码中定义了一个对应类型的变量时,就可以计算这个表达式。

image

所以可以看出,强制转型表达式必须配合在代码中使用对应类型才可以正常计算。这里的使用不一定指定义,只要调用或者传递此类型的对象都可以。

模版类型的调试视图加载

VS的调试视图在使用类型的时候还会遇到一个问题就是在表达式中只能识别完整的类型名称,标准库的大量类中其实都带有默认的模板参数,例如我们常用的std::map<std::string,std::string>的完整类型名称是std::map<std::basic_string<char,std::char_traits<char>,std::allocator<char> >,std::basic_string<char,std::char_traits<char>,std::allocator<char> >,std::less<std::basic_string<char,std::char_traits<char>,std::allocator<char> > >,std::allocator<std::pair<std::basic_string<char,std::char_traits<char>,std::allocator<char> > const ,std::basic_string<char,std::char_traits<char>,std::allocator<char> > > > >

如果隐藏的内部类有多个模板类型的成员的话,由于描述大量的强制转型时需要使用完整的名称,会导致调试视图中非常复杂,并且可读性差。

解决方法是使用typedef或者using来定义类型别名,调试视图的表达式可以识别类型别名,不过识别的前提是当前的运行堆栈的代码上下文有使用到这个类型别名,也就是说,如果只是把别名定义在隐藏的内部类依然无法生效。

image

image

加载调试视图

当调试视图编写完成后,调试视图文件该如何加载呢,有如下几种方法:

  • 项目中手动加载,在C++项目中添加调试视图文件即可。
  • 随VS扩展包安装,制作扩展包时附带,具体见官方文档。
  • 随调试信息pdb文件发布、加载,这是比较推荐的方式。在C++项目中添加调试视图文件,构建项目时会自动将调试视图文件加入pdb文件。
  • 从系统或用户的Natvis目录自动加载: %USERPROFILE%\Documents\Visual Studio 2022\Visualizers<VS Installation Folder>\Common7\Packages\Debugger\Visualizers

总结

通过自定义调试视图,可以使得开发者在调试时获得更加直观、明了的对象状态信息,库的作者通过提供调试视图也可以使得库的使用者使用时更加方便、高效。
同时自定义调试视图还可以解决长久以来困扰动态库隐藏内部实现导致的无法在模块外部查看调试信息的问题(虽然还是有不少限制)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值