65.9K
CodeProject 正在变化。 阅读更多。
Home

Wave:一个符合标准 C++ 预处理器库

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (56投票s)

2003年3月25日

13分钟阅读

viewsIcon

450553

downloadIcon

4414

描述了一个免费且完全符合标准的 C++ 预处理器库。

Sample Image - wave_preprocessor.jpg

引言

您是否曾想过拥有自己的 C/C++ 预处理器?或者您是否好奇您工具箱中这个看不见的日常助手是如何工作的?如果是,您可能想继续阅读。如果不是——在点击浏览器“后退”按钮之前,考虑学习一些新知识,也继续阅读吧 :-) 。

C++ 预处理器是一个宏处理器,在正常情况下由您的 C++ 编译器自动使用,在实际编译之前转换您的程序。它被称为宏处理器是因为它允许您定义宏,宏是更长构造的简短缩写。C++ 预处理器提供了四种独立的功能,您可以根据需要使用:

  • 包含头文件
  • 宏展开
  • 条件编译
  • 行控制

如今,这些功能被大大低估了,甚至预处理器也长期以来受到批评,以至于它的使用直到几年前 Boost 预处理器库 [1] 出现之前,都没有得到有效的推广。直到今天,我们才开始理解,预处理器生成元编程与 C++ 中的模板元编程相结合,是迄今为止任何语言所支持的最强大的编译时反射/元编程功能之一。

C++ 标准 [2] 于 1998 年被采纳,但据我所知,仍然没有 C++ 编译器能够无错误地实现其中规定的相当简单的预处理器要求。这可能是由于前面提到的低估,甚至是近几年来预处理器在良好编程风格中被禁止,或者可能源于描述预处理器时使用的有些笨拙的标准化的英语方言。

因此,*Wave* 预处理器库旨在:

  • 提供对规定预处理器功能的一个免费、完全符合标准且(希望如此)无错误的实现
  • 最大限度地利用 C++ STL 和/或 Boost [3] 库(以实现紧凑性和可维护性)
  • 实现附加功能的易于扩展性
  • 构建一个灵活的库,以满足不同的 C++ 词法分析和预处理需求。

为了简化输入流(通常是文件,但不限于此)的解析任务,使用了 Spirit 解析器构造库 [4]

背景

*Wave* C++ 预处理器不是一个单体应用程序,而是一个模块化库,主要公开一个上下文对象和一个迭代器接口。上下文对象有助于配置实际的预处理过程(如搜索路径、预定义宏等)。公开的迭代器也是由这个上下文对象生成的。遍历这两个迭代器定义的序列将返回预处理后的令牌,这些令牌是根据给定的输入流即时构建的。

C++ 预处理器迭代器本身由一个 C++ 词法分析器迭代器馈送,该迭代器实现了统一的接口。顺便说一句,*Wave* 库中包含的 C++ 词法分析器也可以独立使用,并且完全不与 C++ 预处理器迭代器绑定。作为词法分析器,我将理解一段代码,它将输入流中的几个连续字符组合成一个更适合后续解析的对象流(称为令牌)。这些令牌不仅携带有关匹配字符序列的信息,还携带有关在输入流中找到特定令牌的位置的信息。换句话说,词法分析器会移除所有对人类来说非常有用的垃圾,如空格、换行符等(即执行一些词法转换),而将结构转换留给解析器。

为了使 *Wave* C++ 预处理库模块化,C++ 词法分析器完全独立于预处理器。为了证明这一概念,目前库中实现了两个功能完全相同的不同 C++ 词法分析器。C++ 词法分析器公开了前面提到的统一接口,因此 C++ 预处理器迭代器可以与两者一起使用。将 C++ 词法分析器从 C++ 预处理器迭代器库中抽象出来是为了允许插入其他不同的 C++ 词法分析器,而无需重新实现预处理器。这将允许对预处理过程本身进行基准测试和特定调优。

在过去的几周里,*Wave* 获得了另一个应用领域:测试不同标准提案的可用性和适用性。实现了一个新的 C++0x 模式,该模式允许尝试并帮助建立一些旨在克服 C++ 预处理器已知限制的想法。

使用代码

实际的预处理是一个高度可配置的过程,所以显然您需要定义一些参数来控制这个过程,例如:

  • 包含搜索路径,定义了在哪里搜索要用 #include <...>#include "..." 指令包含的文件
  • 要预定义哪些宏,以及要取消定义哪些预定义宏
  • 几个其他选项,例如控制是否启用 C++ 标准的某些扩展(例如变长参数和占位符)等。

您可以通过 wave::context 对象访问所有这些处理参数。因此,您必须至少实例化一个此类对象才能使用 *Wave* 库。有关上下文模板的更多信息,请参阅随附的可下载文件中包含的类参考,或在此处 找到。上下文对象是一个模板类,您必须为其提供至少两个模板参数:要使用的底层输入流的迭代器类型,以及从预处理引擎返回的令牌类型。使用的输入流类型由您定义,令牌类型也是如此,但作为起点,我建议使用 Wave 库中预定义的默认令牌类型——wave::cpplexer::lex_token<> 模板类。您可以在此处 找到 或在随附的可下载文件中找到该类的完整参考。

主要的预处理迭代器不应直接实例化,而应通过此上下文对象生成。以下代码片段预处理给定的输入文件,并将生成的文本输出到 std::cout

    // Open the file and read it into a string variable
    std::ifstream instream("input.cpp");
    std::string input(
        std::istreambuf_iterator<char>(instream.rdbuf());
        std::istreambuf_iterator<char>());

    // The template wave::cpplexer::lex_token<> is the default 
    // token type to be used by the Wave library.
    // This token type is one of the central types throughout 
    // the library, because it is a template parameter to many 
    // of the public classes and templates and it is returned 
    // from the iterators itself.
    typedef wave::context<std::string::iterator, 
                wave::cpplexer::lex_token<> >
            context_t;

    // The C++ preprocessor iterators shouldn't be constructed 
    // directly. These are to be generated through a 
    // wave::context<> object. Additionally this wave::context<> 
    // object is to be used to initialize and define different 
    // parameters of the actual preprocessing.
    context_t ctx(input.begin(), input.end(), "input.cpp");
    context_t::iterator_t first = ctx.begin();
    context_t::iterator_t last = ctx.end();

    // The preprocessing of the input stream is done on the fly 
    // behind the scenes during the iteration over the 
    // context_t::iterator_t based stream. 
       while (first != last) {
           std::cout << (*first).get_value();
           ++first;
       }

此示例显示了如何将输入读取到字符串变量中,然后将其馈送到预处理器。但是,wave::context<> 对象的构造函数的参数不限于此类输入流。它可以接受一对任意迭代器类型(概念上至少是 forward_iterator 类型迭代器)指向要从中读取预处理数据的输入流。第三个参数提供一个文件名,该文件名随后可从预处理返回的预处理令牌中访问,以指示输入流中令牌的位置。但请注意,此文件名仅在未遇到 #include#line 指令时使用,这些指令又会更改当前文件名。

对预处理令牌的迭代相对直接。只需从上下文对象获取起始和结束迭代器(也许在初始化了一些包含搜索路径后),然后您就完成了!迭代器的解引用将返回从输入流中即时生成的预处理令牌。

正如您可能已经看到的,整个库都位于 C++ namespace wave 中。因此,在使用不同的类时,您必须显式指定它。另一种方法是肯定在源文件开头处放置 using namespace wave;

Wave 跟踪设施

如果您曾经需要调试宏展开,您会发现您的工具对此任务的支持很少或几乎没有。因此,*Wave* 库提供了一个跟踪设施,允许选择性地获取有关某个宏或多个宏展开的信息。

宏展开的跟踪会生成大量信息,因此建议您仅为要跟踪的宏显式启用/禁用跟踪。这可以通过特殊的 #pragma 来完成。

#pragma wave trace(enable)    // enable the tracing
// the macro expansions here will be traced
// ...
#pragma wave trace(disable)   // disable the tracing

为了查看 *Wave* 驱动程序在展开简单宏时生成的内容,我建议您尝试使用“wave -t test.trace test.cpp”进行编译。

// test.cpp
#define X(x)          x
#define Y()           2
#define CONCAT_(x, y) x ## y
#define CONCAT(x, y)  CONCAT_(x, y)
#pragma wave trace(enable)
// this macro expansion is to be traced
CONCAT(X(1), Y())     // should expand to 12
#pragma wave trace(disable)

执行此命令后,test.trace 文件将包含生成的跟踪输出。生成的输出相对容易理解,但您可以在随附的可下载文件中包含的文档中找到对跟踪输出格式的详细描述。

实验性 C++0x 模式

为了准备并支持 C++ 标准委员会的一项提案,该提案将描述某些新的和增强的预处理器功能,*Wave* 预处理器库已经实现了对以下功能的实验性支持:

  • C++ 中的变长宏和占位符令牌
  • 定义明确的令牌粘贴
  • 宏作用域机制
  • 新的替代预处理器令牌

变长宏和占位符令牌已经从 C99 标准中为人所知。将它们添加到 C++ 标准将有助于使 C99 和 C++ 之间的差异更小。

未关联令牌的令牌粘贴(即导致多个预处理令牌的令牌粘贴)目前是未定义行为,没有实质性的原因。它不依赖于体系结构,也不难以实现诊断。此外,重令牌化是大多数(如果不是全部)预处理器已经做到的,也是大多数程序员期望预处理器做到的。明确定义的行为只是将现有实践标准化,并从标准中删除任意且不必要的未定义行为。

预处理器的主要问题之一是宏定义不遵守核心语言的任何作用域机制。正如历史所表明的那样,这是一个重大的不便,并大大增加了翻译单元中名称冲突的可能性。解决方案是向 C++ 预处理器添加命名和非命名作用域机制。这限制了宏定义的范围,但并未限制其可访问性。

提议的作用域机制借助三个新的预处理器指令实现:#region#endregion#import(请注意,指令的实际名称可能会在标准化过程中更改)。此外,它还改变了一些现有预处理器指令的细节:#ifdef#ifndefoperator defined()

为避免在此文章中对新功能进行过于详细的描述,这里提供了一个简单示例(来自 Paul Mensonides Paul Mensonides 编写的预处理器库的实验版本),该示例演示了提议的扩展。

    # ifndef ::CHAOS_PREPROCESSOR::chaos::WSTRINGIZE_HPP
    # region ::CHAOS_PREPROCESSOR::chaos
    #
    # define WSTRINGIZE_HPP
    #
    # include <chaos/experimental/cat.hpp>
    #
    # // wstringize
    #
    # define wstringize(...) \
        chaos::primitive_wstringize(__VA_ARGS__) \
        /**/
    #
    # // primitive_wstringize
    #
    # define primitive_wstringize(...) \
        chaos::primitive_cat(L, #__VA_ARGS__) \
        /**/
    #
    # endregion
    # endif

    # import ::CHAOS_PREPROCESSOR
 
    chaos::wstringize(a,b,c) // expands to: L"a,b,c"

宏作用域语法模仿了核心 C++ 语言中已知的命名空间作用域。但有一个显著的区别。#region#endregion 指令对于来自作用域外部或内部的任何宏定义都是不透明的。这样,在特定作用域内定义的宏只有在被导入(通过 #import 指令)或被限定(例如上面 #ifndef 指令的参数)时,才能从该作用域外部可见。

有关新实验功能的更多详细信息,请参阅随附的可下载文件中包含的文档。

描述的功能通过 *Wave* 驱动程序的 --c++0x 命令行选项启用。或者,您可以通过将 wave::support_cpp0x 值传递给 wave::context<>::set_language() 函数来启用这些功能。

命令行预处理器驱动程序

要了解如何编写一个功能齐全的预处理器,您可以参考随附的可下载文件中的 *Wave* 驱动程序示例。这个 *Wave* 驱动程序完全利用了该库的功能。它可以作为任何其他 C++ 编译器之上的预处理器可执行文件使用。它输出从给定输入文件生成的预处理令牌的文本表示。此驱动程序程序具有以下命令行语法:

Usage: wave [options] [@config-file(s)] file:
 
  Options allowed on the command line only:
    -h [--help]:            print out program usage (this message)
    -v [--version]:         print the version number
    -c [--copyright]:       print out the copyright statement
    --config-file filepath: specify a config file (alternatively: @filepath)
 
  Options allowed additionally in a config file:
    -o [--output] path:          specify a file to use for output instead of 
                                 stdout
    -I [--include] path:         specify an additional include directory
    -S [--sysinclude] syspath:   specify an additional system include directory
    -F [--forceinclude] file:    force inclusion of the given file
    -D [--define] macro[=[value]]:    specify a macro to define
    -P [--predefine] macro[=[value]]: specify a macro to predefine
    -U [--undefine] macro:       specify a macro to undefine
    -n [--nesting] depth:        specify a new maximal include nesting depth
    
  Extended options (allowed everywhere)
    -t [--traceto] path:    output trace info to a file [path] or to stderr [-]
    --timer:                output overall elapsed computing time to stderr 
    --variadics:            enable variadics and placemarkers in C++ mode
    --c99:                  enable C99 mode (implies variadics)
    --c++0x:                enable experimental C++0x support (implies 
                            variadics)
 

为了允许跟踪输出,*Wave* 驱动程序现在有一个特殊的命令行选项 -t (--trace),应使用它来指定将生成跟踪信息的文件。如果使用单个破折号('-')作为文件名,则输出将转到 std::cerr 流。

还有一个需要注意的地方。要使用 *Wave* 库或自行编译 *Wave* 驱动程序,您至少需要 VC7.1 编译器(VS.NET 2003 发行版中包含的 C++ 编译器)。或者,您可以使用较新版本的 gcc 编译器(GNU Compiler Collection)或 Intel V7.0 C++ 编译器进行编译。抱歉,目前不支持 VC6 和 VC7,它们离 C++ 标准符合性太远了。但我最终会尝试修改 *Wave* 库的部分内容,使其也能与这些编译器一起编译——这取决于您的反馈。

*Wave* 依赖于 Boost 库(至少 V1.30.2)和 Vladimir Prus 的 Program Options 库(至少 rev. 160,最近已合并到 Boost,但尚未包含),因此在尝试重新编译 *Wave* 之前,请务必安装这些库。

结论

尽管 *Wave* 库相当复杂且大量使用高级 C++ 惯用法,如模板和基于模板的元编程,但在广泛的应用中它相当容易使用。它很好地融入了 C++ 标准模板库 (STL) 多年来使用的知名范式。

据我所知,*Wave* 驱动程序是唯一一个 C++ 预处理器,它:

  • 允许为 C++ 程序启用变长参数和占位符
  • 公开支持宏展开过程调试的功能
  • 实现实验性 C++0x 支持,如宏作用域,它将被提议作为 C++ 标准的补充

因此,它可能是开发现代 C++ 程序的一个无价的工具。

正如 Boost Preprocessor Library [1] 等近期发展所示,未来我们将看到大量高级预处理器技术的应用。但这些需要一个坚实的基础——一个符合标准的预处理器。只要广泛可用的编译器不满足这些需求,*Wave* 库就可以填补这一空白。

参考文献

  1. C/C++ Boost 库预处理器子集
  2. 编程语言 - C++ (INCITS/ISO/IEC 14882:1998)
  3. Boost 库文档
  4. Spirit 解析器构造框架
  5. Wave C++ 预处理器库

历史

2003/03/25 (Wave V0.9.1)

  • 本文初稿

03/26/2003

  • 修复了参考文献中的一个损坏链接

2003/04/07 (Wave V0.9.2)

  • 修复了文章文本中的几个拼写错误
  • 添加了跟踪设施以跟踪宏展开过程
  • 添加了预定义宏 __INCLUDE_LEVEL__
  • 添加了对 operator _Pragma() 的支持(仅限 C99 和 --variadics 模式)
  • 为 *Wave* 驱动程序添加了新的命令行选项
  • 修复了几个错误(请参阅可下载文件中包含的 ChangeLog 文件)
  • 更新了文档(包含在可下载文件中)

2003/05/16 (Wave V0.9.3)

  • 添加了 _Pragma wave system()
  • 添加了预包含文件的可能性
  • 添加了实验性 C++0x 模式,包含:
    • 宏作用域支持
    • 定义明确的令牌粘贴
    • 变长参数和占位符
    • __comma____lparen____rparen__ 替代 pp-令牌
  • 修复了大量错误(请参阅可下载文件中包含的 ChangeLog 文件)

05/22/2003

  • 纠正了几个拼写错误

06/04/2003

  • 修复了几个宏展开错误
  • 更新了附加的源代码和演示文件(请参阅可下载文件中包含的 ChangeLog 文件)

2004/01/05 (Wave V1.0)

  • 添加了对 #pragma once 指令的支持
  • 添加了对 #pragma wave timer() 和 --timer 命令行开关的支持(请参阅可下载文件中包含的文档)
  • 包含了一个有限状态机,它可以抑制不必要的空格。这使得生成的输出更加紧凑。
  • 添加了一个可选的 IDL 模式,该模式除了不识别 C++ 特定令牌外,不识别任何关键字(除了 truefalse),只识别标识符。
  • 整合了几项改进了整体性能的更改
  • 修复了大量错误(请参阅可下载压缩包中包含的 ChangeLog 文件)
  • 将许可切换为使用 Boost 软件许可证 1.0 版
© . All rights reserved.