C ++仍需要预处理器吗? (2017)

2020-12-06 20:21:53

它是必须与C ++一起使用的原始文本替换工具。但是“必须”确实如此吗?由于新的和更好的C ++语言功能,大多数用法已经过时了。模块之类的更多功能很快就会出现™那么我们能摆脱预处理器吗?如果是的话,我们该怎么办呢?

预处理器的大部分使用已经是不好的做法:不要将其用于符号常量,请勿将其用于内联函数等。

因为这引起了很大的吸引力,所以让我澄清一下:我不主张在这篇文章中删除预处理器。但是,C ++社区中的一些人和标准化委员会中的许多人都想这样做,所以,我想探索一下可行性。

但是在惯用的C ++中仍然有一些使用方法。让我们仔细研究一下,看看我们有什么替代方法。

为了编译源文件,编译器需要查看所有正在调用的函数的声明。因此,如果您在一个文件中定义一个函数,并想在另一个文件中调用它,则必须在该文件中将其声明为编译器才能生成适当的代码来调用该函数。

当然,手动复制声明会导致错误:如果更改签名,还必须更改所有声明。因此,与其手动复制声明,不如手动复制声明,而是将它们写入一个特殊的文件-头文件中,然后预处理器使用#include为您复制它。现在,您仍然需要更新所有声明,但只需要在一个地方即可。

但是纯文本包含是愚蠢的。同一文件有时会被包含两次,有时会导致该文件得到两个副本。这对于函数声明没有问题,但是如果您在头文件中有类定义,那就是错误的。

但是我们可以使用Modules TS。我们可以编写模块并将其导入,而不是提供头文件和源文件。

如果您想了解有关模块的更多信息,我强烈建议您使用最新的CppChat。

预处理器的第二个最常见的工作是条件编译:通过定义或不定义宏来更改定义/声明。

考虑一下您正在编写一个提供了函数draw_triangle()的库的情况,该函数在屏幕上绘制了一个三角形。

但是该功能的实现会根据您的操作系统,窗口管理器,显示管理器和/或月相(对于特殊窗口管理器)而变化。

//将其用于Windows void draw_triangle(){//使用WinAPI创建窗口//使用DirectX绘制三角形} // //将其用于Linux void draw_triangle(){//使用X11创建窗口//使用OpenGL绘制三角形}

#if _WIN32 // Windows三角形绘图代码在这里#else // Linux三角形绘图代码在#endif

分支中未采用的代码将在编译前被删除,因此我们不会收到有关缺少API等的任何错误。

如果DEBUG_MODE为false,则分支将无法正确编译,它将仅检查语法错误,类似于对尚未实例化的模板进行的检查。

如前所述,这是不正确的。如果不在模板中,则仍将对其进行全面检查。不过,这在这里无关紧要,因为它仍然没有任何运行时开销。

这甚至比#if更好,因为它将在不检查所有宏组合的情况下发现代码中明显的错误。constexpr的另一个好处是DEBUG_MODE现在可以是普通的constexpr变量,而不是来自宏扩展的常量。

如果您没有constexpr,则可以使用类模板专业化或标签分发。

当然,如果使用constexpr,则有缺点:您不能使用它来约束预处理程序指令,即#include。对于draw_triangle()示例,代码需要包含正确的系统标头。如果constexpr可以帮助您,则您需要在那里进行真正的条件编译,或手动复制声明。

由于系统标头声明通常很稳定,因此比通常情况要好。但是仍然不建议这样做。

而且模块也无济于事,因为系统头文件未定义您可以导入的任何模块。此外,据我所知,您不能有条件地导入模块。

与此相关的是,有时您希望将一些配置选项传递给库。您可能希望启用或禁用断言,前提条件检查,更改某些默认行为……

#ifndef USE_ASSERTIONS //默认启用#define USE_ASSERTIONS 1 #endif #ifndef DEFAULT_FOO_IMPLEMENTATION //使用常规实现#define DEFAULT_FOO_IMPLEMENTATION general_foo #endif…

构建库时,然后可以在调用编译器时或通过CMake覆盖宏。

我们可以使用其他策略来传递选项,例如基于策略的设计,在该选项中,您将策略传递给定义所选行为的类模板。这样做的好处是,它不会对所有用户强制执行单个实现,而是当然有其缺点。

但是,我真正想看到的是在导入模块时可以通过以下配置选项的功能:

但我认为,在不牺牲模块所提供的好处(即预编译模块。

您最常使用的宏可能会执行某种断言。宏是此处的明显选择: 您需要有条件地禁用断言并将其删除,以使它们在发布时的开销为零。 如果有宏,则可以使用预定义的__LINE __,__ FILE__和__func__来获取断言所在的位置,并在诊断中使用它。 如果您有宏,则还可以对要检查的表达式进行字符串化,并在诊断中也使用它。 我已经探讨了如何替换条件编译,以及如何指定是否应启用条件编译,因此这没问题。 在这里使用基于策略的设计还可以自定义如何将诊断报告给用户。 在Library Fundamentals TS v2中也可以获取文件信息,因为它添加了std :: experimental :: source_location:

函数std :: experimental :: source_location :: current()会在编写时扩展到​​有关源文件的信息,此外,如果将其用作默认参数,它将扩展到调用方位置。第二点也没问题。

第三点很重要:如果不使用宏就无法对表达式进行字符串化和在诊断中进行打印。如果可以,今天就可以实现断言功能。

但除此之外,您仍然需要一个宏。请查看此博客文章,了解如何实现(几乎)无宏的断言函数,您可以在其中使用constexpr变量而不是宏来控制级别。您可以在此处找到完整的实现。

并非所有的编译器都支持所有C ++功能,这给移植带来了很大的麻烦,特别是如果您无权访问编译器进行测试并且需要执行“更改一行,推送至CI,等待CI构建,更改另一个”循环”只是因为某些编译器确实不喜欢重要的C ++功能!

无论如何,通常的兼容性问题都可以通过宏解决。实现甚至定义了某些宏,一旦实现了某种功能,就可以轻松地进行检查:

#if __cpp_noexcept #define NOEXCEPT noexcept #define NOEXCEPT_COND(Cond)noexcept(Cond)#define NOEXCEPT_OP(Expr)noexcept(Expr)#else #define NOEXCEPT #define NOEXCEPT_COND(Cond)#define NOEXCEPT_OP(Expr)fun #end ()NOEXCEPT {…}

即使不是所有编译器都已经具备功能,这也允许便携式使用功能。

我们无法通过其他方式做到这一点。解决方案缺少的功能需要某种预处理工具才能摆脱不支持的功能。我们必须在此处使用宏。

C ++的模板和TMP可以消除很多原本需要编写的样板代码,但是有时候,您只需要编写很多相同但不完全相同的代码即可:

struct less {bool operator()(const foo& a,const foo& b){返回a。酒吧< b。酒吧; }; struct更大{bool operator()(const foo& a,const foo& b){返回a。栏> b。酒吧; }; …

#define MAKE_COMP(Name,Op)\结构名称\ {\ bool operator()(const foo& a,const foo& b)\ {\返回a.bar \} \}; MAKE_COMP(less,<)MAKE_COMP(Greater,>)MAKE_COMP(less_equal,< =)MAKE_COMP(Greater_equal,> =)#undef MAKE_COMP

或者您需要为枚举生成to_string()实现,这是一个带有X宏的简单任务:

//在enum_members.hpp中X(foo)X(bar)X(baz)//在header.hpp中枚举类my_enum {//按原样扩展枚举名称#define X(x)x,#include" enum_members.hpp" #undef X}; const char * to_string(my_enum e){switch(e){//生成case #define X(x)\ case my_enum :: x:\ return #x; #include" enum_members.hpp" #undef X}; };

它们只是使很多代码更易于阅读和使用:您不需要复制粘贴,不需要精美的工具,并且对用户没有真正的“危险”。

我们不能用单一语言功能替代所有这些功能。对于第一个功能,我们需要一种将重载函数(如运算符)传递给模板的方法,然后可以将其作为模板参数传递并对其进行简单别名。对于第二个,我们需要概念;对于第三个,我们需要反思。

因此,如果不依靠手动编写样板代码,就无法摆脱此类样板宏。

模块TS允许替换最常用的用法-#include,但有时仍需要预处理器,尤其是要确保平台和编译器的兼容性。

即便如此,我仍然认为适当的宏是有用的东西,它们是编译器的一部分,并且是AST生成的功能非常强大的工具,例如类似于Herb Sutter的元类,但是我绝对不希望原始的#define的文字替换。

这篇博客文章是为我的旧博客设计而写的,并移植了过来。如果有任何问题,请通知我。