排错实战 —— 解决 c++ 工程编译错: error C2059 'string' illegal token on right

简介: 排错实战 —— 解决 c++ 工程编译错: error C2059 'string' illegal token on right

缘起

最近,项目里出现了一个奇怪的编译错误。乍看错误提示,真有丈二的和尚,摸不着头脑的感觉。解决之后,又是这么的合情合理。具体是什么样的问题呢?一起来看看吧。

说明:实际项目中的错误隐藏的更深,完全没有相关的错误提示。因为不方便用项目代码演示,准备了一个简单的例子,大家可以新建一个控制台工程,并把下面的代码粘贴到对应的文件里。

示例代码简介

示例代码比较简单,共有五个关键文件,加起来不到 40 行代码。大家可以先观察一下代码,思考编译是否会遇到问题。

// NameCollisionDemo.cpp
#include "ModifyInfoTest.h"
int wmain(int argc, wchar_t* argv[])
{
  Test();
  return 0;
}

// ModifyInfo.h
#pragma once
class CModifyInfo
{
public:
  enum class eSource { None = 0, BayWindow, Beam };
  CModifyInfo(eSource source_) : source(source_) {}
  eSource source;
};

// ModifyInfoTest.h
#pragma once
#include "ModifyInfo.h"
#include "UiMacros.h"

static void Test()
{
    CModifyInfo info1(CModifyInfo::eSource::BayWindow);
    CModifyInfo info2(CModifyInfo::eSource::Beam);
}

// UiMacros.h
#pragma once
#include "BayWindowUiMacros.h"

// BayWindowUiMacros.h
#pragma once
#define BayWindow "Ui.BayWindow"

初识错误

vs 打开工程后,编译,报错如下:

compile-error-c2589

大家能从图中得到什么信息呢?

  • 第一行提示 error C2059: syntax error : '::'。语法错误?
  • 第二行提示 error C2589: 'string' : illegal token on right side of '::'::右侧有非法符号?
  • 第三行提示 IntelliSense: expected an identifier。期待一个标识符?

注意:第三行是 IntelliSense 提示的,不是真正意义上的错误。IntelliSense 提示的错误对是否能成功编译没有影响。注意看图标,不是 大红叉。第三行给出了出错文件(ModifyInfoTest.h)及行号( 6 ),列号 45。书中暗表,这个提示是最接近出错地点的。

关键的悬浮提示

对照代码仔细检查,没问题啊。BayWindow 确实在 SourceType 中定义了,大小写也没问题。使用 Visual AssistX 的快捷键 alt + g 能正常跳转到定义。这是什么情况?如果我们把鼠标放到 BayWindow 上,有可能会看到下图中的提示:

鼠标悬浮提示

Oops,怎么 BayWindow 变成了一个宏?应该是在编译到这条语句前,遇到了一个名字为 BayWindow 的宏。我们接下来的任务是找到这个宏是在哪里定义的,又为什么会出现在这条语句前。

说明: 在实际项目里通过什么方法看都是正常的。alt + g 和 F12 都能正常跳转到定义,鼠标悬停提示也正常。应该是实际的工程太复杂了,智能感知不好使了!
hover-tip-normal-in-real-project

深入调查

solution 范围搜索关键字 BayWindow。可以勾选 Match whole word(全字匹配)和 Match case (大小写匹配) 排除无关的信息。

搜索整个 solution

纳尼?没有这个宏。大写的尴尬(不要问我怎么写)!原来,我们指定搜索范围为 Entire Solution 的时候,vs 只会搜索已经添加到工程的文件。我们需要指定搜索范围为 Entire Solution ( Including External Items )。这样就可以搜到没加到工程里,但是被 include 的头文件了。搜索结果如下图:

搜索 solution 及包含文件

好了,至此我们已经知道 BayWindow 宏定义在 BayWindowUiMacros.h 中了。我们的下一个目标是:找出为什么在编译出错语句前,BayWindow 宏就被定义了。

应该是在出问题的代码前面的某个位置包含了 BayWindowUiMacros.h。我们需要找到这个关键的 #include BayWindowUiMacros.h 语句。

我们可以搜索 BayWindowUiMacros.h,发现只有 UiMacros.h 包含了 BayWindowUiMacros.h。继续搜索 UiMacros.h,我们发现在 ModifyInfoTest.h 的第 3 行包含了 UiMacros.h

找到真相

至此,我们查清了来龙去脉——在 ModifyInfoTest.h 的第 3 行包含了 UiMacros.h,从而间接包含了 BayWindowUiMacros.h,里面定义了名为 BayWindow 的宏。第 6 行的 BayWindow 在预处理阶段被当成宏处理了,所以第 6 行就变成了 WallModifyInfoEx info1(WallModifyInfoEx::SourceType::"Ui.BayWindow")

到底是不是这样的呢?有没有办法验证我们的猜测呢?

说明: 在实际工程中,头文件的包含关系极有可能比这个简单的示例工程复杂的多。手动排查绝对是体力活!我在解决项目里的编译问题的时候,是通过下面的方法排查的。在准备示例工程的时候,通过悬浮提示发现了居然直接提示有问题的地方是一个宏,大大降低了排查难度。有时候,智能感知还是挺有用的。

杀手锏

解决这种问题有一个可以称得上杀手锏的设置 —— Preprocess to a File。这个设置可以把预处理后的文件以编译单元(.cpp, .c 等)为单位输出到 Intermediate Directory (中间目录)。可以在工程设置里修改中间目录的值。

Intermediate Directory 设置

在工程上,右键 -> 属性 打开工程设置。然后设置 Configuration Properties -> C/C++ -> Preprocessor 中的 Preprocess to a FileYes(/P) 就可以把预处理的结果输出到中间目录了。

preprocess to file 设置

上图中右侧黄色高亮的两个选项会影响生成的中间文件的内容,Preprocess Suppress Line NumbersYes(/P) 表示输出的中间文件不包含行号信息,Keep CommentsTrue 表示保留注释,否则不保留。

设置好后,重新编译。就可以生成 .i 文件了。让我们一起查看下生成的中间文件(我生成的时候,输出了行号信息):

查看中间文件

我们发现,有问题的那一行居然变成了 WallModifyInfoEx info1(WallModifyInfoEx::SourceType::"Ui.BayWindow")。证实了我们的猜想。

说明:

  1. 我们怎么知道应该看哪个中间文件呢?真实的项目里,会有 N 多个源文件,我们不可能每个文件都检查一遍。我们可以根据上一篇文章里介绍的 输出窗口Build Order 定位到错误出现在哪个源文件中。示例工程比较简单,就省略了这一步。
  2. 在真实项目里,生成的中间文件会很大。基本不可能用肉眼看,需要靠搜索关键字定位。我们可以把 Keep Comments 设置为 True,然后在出问题的哪一行的后面写一个带特殊标记的注释,搜索特殊标记就可以了。

总结

  • 要充分利用 IDE 给出的提示,Intellisense 还是很有用的,虽然在大工程里经常性的失效。

  • 定义宏的时候,尽量全大写,这样与其它名字冲突的机率会小很多。

  • 源文件必须添加到工程文件中,但是头文件不是必须添加到工程文件里的。

  • 搜索范围指定为 Entire Solution ( Including External Items ),可以在没添加到工程中的头文件中搜索。

  • Preprocess to a File 你记住了吗?

相关文章
|
4月前
|
监控 Linux 测试技术
C++零拷贝网络编程实战:从理论到生产环境的性能优化之路
🌟 蒋星熠Jaxonic,技术宇宙中的星际旅人。深耕C++与零拷贝网络编程,从sendfile到DPDK,实战优化服务器性能,毫秒级响应、CPU降60%。分享架构思维,共探代码星辰大海!
|
7月前
|
C语言 C++
【实战指南】 C/C++ 枚举转字符串实现
本文介绍了在C/C++中实现枚举转字符串的实用技巧,通过宏定义与统一管理枚举名,提升代码调试效率并减少维护错误。
446 65
|
7月前
|
程序员 编译器 C++
【实战指南】C++ lambda表达式使用总结
Lambda表达式是C++11引入的特性,简洁灵活,可作为匿名函数使用,支持捕获变量,提升代码可读性与开发效率。本文详解其基本用法与捕获机制。
283 58
|
11月前
|
监控 Linux C++
【实战指南】4步实现C++插件化编程,轻松实现功能定制与扩展(2)
本文是《4步实现C++插件化编程》的延伸,重点介绍了新增的插件“热拔插”功能。通过`inotify`接口监控指定路径下的文件变动,结合`epoll`实现非阻塞监听,动态加载或卸载插件。核心设计包括`SprDirWatch`工具类封装`inotify`,以及`PluginManager`管理插件生命周期。验证部分展示了插件加载与卸载的日志及模块状态,确保功能稳定可靠。优化过程中解决了动态链接库句柄泄露问题,强调了采纳用户建议的重要性。
436 89
【实战指南】4步实现C++插件化编程,轻松实现功能定制与扩展(2)
|
11月前
|
人工智能 程序员 C++
【实战经验】C/C++右移高位补0还是1?
本文探讨了C/C++中右移运算时高位补0还是补1的问题。通过示例代码分析,揭示了右移规则:无符号类型高位补0;有符号类型根据正负决定(正数补0,负数补1)。文中列举了可能导致错误的场景,并提供了两种规避措施——使用无符号类型和掩码校正,确保结果符合预期。最后总结指出,右移运算虽常见,但若处理不当易引发隐晦Bug,需谨慎对待。
647 80
|
12月前
|
存储 安全 C语言
C++ String揭秘:写高效代码的关键
在C++编程中,字符串操作是不可避免的一部分。从简单的字符串拼接到复杂的文本处理,C++的string类为开发者提供了一种更高效、灵活且安全的方式来管理和操作字符串。本文将从基础操作入手,逐步揭开C++ string类的奥秘,帮助你深入理解其内部机制,并学会如何在实际开发中充分发挥其性能和优势。
|
9月前
|
监控 算法 数据处理
基于 C++ 的 KD 树算法在监控局域网屏幕中的理论剖析与工程实践研究
本文探讨了KD树在局域网屏幕监控中的应用,通过C++实现其构建与查询功能,显著提升多维数据处理效率。KD树作为一种二叉空间划分结构,适用于屏幕图像特征匹配、异常画面检测及数据压缩传输优化等场景。相比传统方法,基于KD树的方案检索效率提升2-3个数量级,但高维数据退化和动态更新等问题仍需进一步研究。未来可通过融合其他数据结构、引入深度学习及开发增量式更新算法等方式优化性能。
238 17
|
8月前
|
对象存储 C++ 容器
c++的string一键介绍
这篇文章旨在帮助读者回忆如何使用string,并提醒注意事项。它不是一篇详细的功能介绍,而是一篇润色文章。先展示重载函数,如果该函数一笔不可带过,就先展示英文原档(附带翻译),最后展示代码实现与举例可以直接去看英文文档,也可以看本篇文章,但是更建议去看英文原档。那么废话少说直接开始进行挨个介绍。
162 3
|
8月前
|
存储 算法 安全
c++模板进阶操作——非类型模板参数、模板的特化以及模板的分离编译
在 C++ 中,仿函数(Functor)是指重载了函数调用运算符()的对象。仿函数可以像普通函数一样被调用,但它们实际上是对象,可以携带状态并具有更多功能。与普通函数相比,仿函数具有更强的灵活性和可扩展性。仿函数通常通过定义一个包含operator()的类来实现。public:// 重载函数调用运算符Add add;// 创建 Add 类的对象// 使用仿函数return 0;
273 0
|
自然语言处理 编译器 C语言
为什么C/C++编译腰要先完成汇编
C/C++ 编译过程中先生成汇编语言是历史、技术和实践的共同选择。历史上,汇编语言作为成熟的中间表示方式,简化了工具链;技术上,分阶段编译更高效,汇编便于调试和移植;实践中,保留汇编阶段降低了复杂度,增强了可移植性和优化能力。即使在现代编译器中,汇编仍作为重要桥梁,帮助开发者更好地理解和优化代码。
为什么C/C++编译腰要先完成汇编