取消定义C ++预处理器

只有两种语言:人们抱怨的语言和没人使用的语言-Bjarne Stroustrup

我喜欢那句话。 它解释了JavaScript和Haskell。 通过这种方式,预处理程序是一种很棒的语言,因为人们经常使用它。 从来没有将它与C和C ++分开考虑,但如果是这样,它将成为TIOBE上的第一语言。 预处理器非常有用且普遍。 事实是,如果没有预处理器的参与,那么编写任何类型的严肃且可移植的C ++应用程序将是“非常”困难的。

—预处理器很烂
- 我知道,对吧? 这是最糟糕的。 嘿,可以合并我的提交吗? 我添加了一堆有用的宏。

我认为很多人都熟悉这种对话,如果我们不小心的话,从现在起20年后也许还会有他们。 不幸的是,因为现有是预处理器的唯一赎回质量。 my,我的问题既不是理论上的,哲学上的也不是理想主义的。

我根本不在乎预处理器是否允许任何人不用任何检查就替换标识符,关键字(实际上,这是非法的……)。 我也不在乎预处理器是否能够在未正确处理逗号的情况下完成图灵处理。 我什至都不在乎包含和包含警卫,而且#pragma也没有一个问题。 有时你必须务实。

然而。

让我为您提供一个方案,您可能会发现它是人为的,但请忍受我。 因此,想象一下,您正在重构某个跨平台应用程序,并且决定做一些不寻常的事情,例如重命名功能。

那不可能 从来没有,也许永远不会。

从根本上讲,无论是编译器还是您的工具(这个工具都必须是成熟的编译器前端)都无法全面了解您的代码。 禁用的部分不会被编译,解析,分类或以其他方式进行分析。

首先,禁用的路径没有义务成为有效的C ++。 这是有效的:

因此,如果编译器要考虑预处理器的禁用路径,则可能无法获得有效的AST。 更糟糕的是,顾名思义,预处理是作为单独的状态发生的,并且预处理指令可能会插入任何两个C ++标记之间,包括任何表达式或语句的中间。

另一个同样令人关注的问题是,编译器可能无法知道应该将#ifdef#defines语句的哪种组合形成有效的程序。

例如,Qt提供了一组defines ,这些defines可以设置为在编译时启用或禁用Qt的某些功能。 假设您不需要日历小部件,则可以定义#QT_NO_CALENDAR_WIDGET ,这样可以生成较小的二进制文件。 没用 我怀疑它永远都行不通。 看到,在某个时候,Qt有大约100个这样的编译时配置选项。 鉴于构建配置的数量可能随变量的数量呈指数爆炸式增长。 当您的程序可能有2´1的变体时,即使在大网络,深云,六规模的情况下,自动化也很困难。

未经测试的代码是损坏的代码。

您可能知道那句著名的格言。 那什至没有编译代码怎么办?

我应该指出,将某些特定于平台的方法放在特定于平台的文件中会导致完全相同的问题。 基本上,编译器看到的代码应该是单个独立的真理来源,但是相反,代码是分散的,并且您所拥有的愿景最好是不完整的。

预处理器被认为是有害的,我们该怎么办?

顺便说一句,有缺陷的不仅是预处理器。 所有现代处理器显然也是如此。 也许应该避免做任何某种处理?

无论如何,让我们今天就可以对预处理器指令进行处理。

1. Strongly Prefer常量而不是#define

这个很简单,但是我仍然看到很多使用宏定义的常量。 始终使用static constconstexpr而不是define 。 如果您的构建过程涉及设置一组变量,例如版本号或git哈希,请考虑生成源文件,而不是使用define作为构建参数。

2.函数总是比宏更好

上面的代码片段来自Win32 API 。 即使对于“简单”且短的衬管,您也应该始终偏爱一种功能。

如果您需要对函数参数进行延迟求值,请使用lambda。 具有讽刺意味的是,这是一个使用宏的解决方案,但这是一个开始!

3.消除可移植性问题。

在单独的文件,适当的库和方法中正确隔离特定于平台的麻烦,应减少代码中#ifdef块的出现。 而且,尽管它不能解决我上面提到的问题,但您不太可能在不使用平台专用符号的情况下重命名或转换平台专用符号。

4.限制您的软件可以具有的变体数量。
该依赖关系真的应该是可选的吗?

如果您具有可选的依赖项,可以考虑使用插件系统或将您的项目分为多个,从而启用软件的某些功能,则在缺少依赖项时,无条件地构建组件和应用程序,而不是使用#ifdef禁用某些代码路径。 确保在有无依赖项的情况下测试构建。 为了避免麻烦,请考虑不要将依赖项设为可选

这段代码真的应该只在发布模式下执行吗?

避免使用许多不同的调试/发布代码路径。 请记住,不是已编译的代码是残破的代码。

该功能真的应该停用吗?

除了依赖以外,功能在编译时也不应是可选的。 提供运行时标志或插件系统

5.一次首选杂用,包括

如今, #pragma once不支持#pragma once的奇异C ++编译器已经很少了。 #pragma once使用#pragma once不太容易出错,更轻松,更快。 亲吻包括卫兵再见。

6.在更多宏中优先使用更多代码

尽管可以适应每种情况,但是在大多数情况下,用宏替换一些c ++标记是不值得的。 在语言规则内玩耍,不要尝试过分聪明和容忍重复,它可能具有可读性,可维护性,您的IDE会感谢您。

7.清理宏

应该尽快使用#undef取消宏定义。 切勿在头文件中放置未记录的宏。

宏没有作用域,请使用长的大写名称作为项目名称的前缀。

如果您使用的Qt这样的第三方框架同时具有短名称和长宏名称( signalQT_SIGNAL ),请确保禁用前者,尤其是当它们可能作为API的一部分泄漏时。 不要自己提供这样的简称。 宏名称应与其余代码保持一致,并且不得与boost::signalstd::min冲突

8.避免将ifdef块放在C ++语句的中间。
foo( 42,
#if 0
"42",
#endif
42.0
);

上面的代码有一些问题。 它很难阅读,难以维护,并且会导致诸如clang-format工具出现问题。 而且,它也碰巧被打破了。

而是编写两个不同的语句:

#if 0
foo(42, "42", 42.0);
#else
foo(42, 42.0);
#endif

您可能会发现很难做到的某些情况,但这可能是您需要将代码拆分为更多函数或更好地抽象出有条件编译的东西的信号。

9.优先使用static_assert而不是#error

只需使用static_assert(false)即可使构建失败。

未来的预处理器

尽管先前的建议适用于任何C ++版本,但是如果可以访问足够新鲜的编译器,则有越来越多的方法可以帮助您减少日常使用的宏。

1.优先使用模块而不是包含

尽管模块应缩短编译时间,但它们的确提供了宏不能泄漏的障碍。 在2018年初,尚无具有该功能的可用于生产环境的编译器,但GCC,MSVC和clang已实现或正在实施中。

虽然缺乏经验,但可以合理地希望模块将使工具变得更容易并且更好地启用功能,例如自动包括与缺失符号相对应的模块,清理不需要的模块……

2.尽可能在#ifdef上使用if constexpr

当禁用的代码路径格式正确(不引用未知符号)时, if constexpr#ifdef的更好选择,因为禁用的代码路径仍将是AST的一部分,并由编译器和您的工具(包括您的静态分析器和重构程序。

3.即使在后现代世界中,您也可能需要使用#ifdef,因此请考虑使用后现代世界。

尽管它们根本无法解决当前的问题,但仍对一组宏进行了标准化,以检测编译器提供的一组标准功能。 如果需要,请使用它们。 我的建议是坚持使用您目标的所有编译器提供的功能。 选择一个基准线。 考虑到将现代编译器回移植到目标系统可能比用C ++ 98编写应用程序要容易。

4.使用std :: source_location而不是__LINE__和__FILE__

每个人都喜欢编写自己的记录器。 现在,您可以使用std :: source_location使用较少的宏或不使用宏。

走向无宏应用的漫长道路

一些功能为某些宏用法提供了更好的替代方法,但实际上,您仍然必须早于较晚才使用预处理器。 但是幸运的是,我们还有很多事情可以做。

1.用编译器定义的变量替换-D

用于define的最常用用例之一是查询构建环境。 调试/发布,目标体系结构,操作系统,优化…

我们可以想象有一组通过std::compiler公开的常量来公开其中一些构建环境变量。

if constexpr(std::compiler.is_debug_build()) {  }

同样,我们可以想象有某种extern compiler constexpr变量在源代码中声明,但是由编译器定义或覆盖。 那只会比constexpr x = SOME_DEFINE;具有真正的好处constexpr x = SOME_DEFINE; 是否有办法约束这些变量可以容纳的值。

也许像那样

enum class OS {
Linux,
Windows,
MacOsX
};
[[compilation_variable(OS::Linux, OS::Windows, OS::MacOsX)]]  extern constexpr int os;

我的希望是,向编译器提供有关各种配置变量是什么,甚至变量的有效组合的更多信息,将导致对源代码进行更好的建模(以及工具和静态分析)。

2.更多属性

C ++属性很棒,我们应该拥有更多或更多的属性。 [[visibility]]将是一个不错的起点。 它可以使用constexpr变量作为从导入切换到导出的参数。

3.从Rust的书中摘录

Rust社区永远不会错过一个机会来大力宣传Rust语言的优点。 实际上,Rust确实做得很好。 而编译时配置就是其中之一。

使用属性系统在编译单元中有条件地包含符号确实是一个非常有趣的想法。

首先,它是真正易读的文档。 其次,即使在构建中不包含符号,我们仍然可以尝试对其进行解析,更重要的是,唯一声明为编译器提供了有关实体的足够信息,以启用强大的工具,静态分析和重构。

考虑以下代码:

[[static_if(std::compiler.arch() == "arm")]]
void f() {}

void foo() {
if constexpr(std::compiler.arch() == "arm") {
f();
}
}

它具有惊人的特性:结构良好。 因为编译器知道f是有效实体,并且它是函数名称,所以if constexpr语句,它可以明确地分析所丢弃的主体。

您可以将相同的语法应用于任何类型的C ++声明,并且编译器将能够理解它。

[[static_if(std::compiler.arch() == "arm")]]
int x = /*...*/

在这里,编译器只能解析左侧,因为静态分析或工具不需要其余部分。

[[static_if(std::compiler.is_debugbuild())]]
class X {
};

出于静态分析的目的,我们只需要索引类名及其公共成员。

当然,从活动代码路径中引用废弃的声明会很不正确,但是编译器可以检查该声明对于任何有效的配置都不会发生。 当然,它不是免费的计算,但是您将有力保证所有代码的格式正确。 因为您是在Linux机器上编写代码而破坏Windows构建的,所以变得更加困难。

但是听起来并不容易。 如果废弃实体的主体包含当前编译器不知道的语法怎么办? 也许是供应商扩展或某些新的C ++功能? 我认为解析是在尽力而为的基础上进行的,并且当解析失败时,编译器可以跳过当前语句并警告其不理解的源代码部分。 “我无法重命名110和130之间的Foo”比“我已重命名Foo的某些实例。 也许不是全部,但愿您有幸在整个项目中略读一遍,真的不用理会编译器,只需使用grep”即可。

4. constexpr所有东西。

也许我们需要一个constexpr std::chrono::system_clock::now()来替换__TIME__

我们可能还需要编译时随机数生成器 。 为什么不 ? 谁会关心可复制的构建?

5.通过反射生成代码和符号

自切片面包,模块和概念以来,元类提案是最好的选择。 特别是P0712在许多方面都是令人赞叹的论文。

引入的众多构造之一是declname关键字,该关键字可从任意字符串和数字序列创建标识符

int declname("foo", 42) = 0; 创建一个变量foo42 。 鉴于字符串连接以形成新标识符是宏的最常用用法之一,这确实非常有趣。 希望编译器能够以这种方式在创建(或引用到)的符号方面获得足够的信息,从而仍能正确索引它们。

在未来几年,臭名昭著的X macro也将成为过去。

6.要摆脱宏,我们需要一种新型的宏

由于宏只是文本替换,因此它们的参数会被延迟计算。 尽管我们可以使用lambda来模仿这种行为,但它相当麻烦。 那么,我们可以从函数的惰性评估中受益吗?

这是我去年考虑的话题

我的想法是利用代码注入提供的功能来创建一种新型的“宏”,由于缺乏更好的名称,我将其称为“语法宏”。 从根本上讲,如果给代码片段命名(可以在程序的给定位置注入的一段代码),并允许它接受许多参数,那么您将获得一个宏。 但是会在语法级别检查宏(而不是预处理器提供的令牌源)。

它将如何运作?

好的,这是怎么回事。

我们首先使用constexpr { }创建一个constexpr block 。 这是元类提案的一部分。 constexpr块是一个复合语句,其中所有变量均为constexpr并且没有副作用。 该块的唯一目的是在编译时创建注入片段并修改声明该块的实体的属性。 ( 元类constexpr块之上的语法糖,我认为我们实际上不需要元类。)

在constexpr块中,我们定义了一个宏log 。 请注意,宏不是函数。 它们扩展为代码,不返回任何内容,也不存在于堆栈中。 log是可以限定的标识符,不能是同一范围内任何其他实体的名称。 语法宏与所有其他标识符遵循相同的查找规则。

他们使用->注入运算符。 ->可用于描述所有与代码注入相关的操作,而不会与其当前用法冲突。 在您的情况下,由于log是一种语法宏,它是代码注入的一种形式,因此我们使用log->(){....}定义该宏。

语法宏的主体本身就是constexpr块,它可以包含可以在constexpr上下文中求值的任何C ++表达式。

它可能包含0个,一个或多个由-> {}表示的注入语句 。 注入语句创建一个代码片段,并立即在调用点将其注入,对于语法宏来说,这是扩展宏的位置。

宏可以插入表达式或0或多个语句。 插入表达式的宏只能在期望表达式和倒数表达式之间扩展。

尽管没有类型,但它的性质由编译器确定。

您可以将任何参数传递到可以传递给函数的语法宏。 在扩展之前对参数进行求值,并进行强类型化。

但是,您也可以在表达式上传递反射。 假定能够反映任意表达式。 表达式e反射具有与decltype(e)对应的类型。

在实现方面,在上面的示例中, std::meta::expression<char*>是匹配类型为char*的表达式上任何反射的概念。

评估宏时的最后一招是将表达式在扩展之前隐式转换为它们的反射。

从根本上讲,我们正在移动AST节点,这与当前反射和代码注入的方法一致。

最后,当我们注入print(->c, ->(args)...)注意->标记。 它将反射转换回原始表达式,然后可以对其进行评估。

从呼叫站点, log->("Hello %", "World"); 看起来像常规的void函数调用,只是->表示存在宏扩展。

最后,在评估之前将标识符作为参数传递的能力可以减轻对新关键字的需求:

在评估std_reflexpr_intrasics(x)之前, std::reflexpr->(x)可以扩展为__ std_reflexpr_intrasics(x)

S-Macro是否完全替换预处理器宏?

他们没有,但他们无意这样做。 值得注意的是,由于它们必须是有效的c ++并在多个点(在定义时,扩展之前,之中和之后)进行检查,因此它们积极禁止令牌添加。 它们是有效的C ++,注入有效的C ++并使用有效的C ++作为参数。

这意味着它们不能插入部分语句,操纵部分语句或将任意语句用作参数。

它们确实解决了延迟评估和条件执行的问题。 例如,由于for(;;)并不是一个完整的语句( for(;;);for(;;){}是有用的,但它们不是很有用),因此您无法与它们一起实现foreach

关于名称查找有很多问题。 宏是否应该“看到”其扩展的上下文? 应该和论点知道宏的内部吗? 它是声明上下文。

我认为限制是一件好事。 如果您确实需要发明新的结构,则可能缺少该语言,在这种情况下,请编写建议。 或者,也许您需要一个代码生成器。 或者只是更多的抽象,或者更多的实际代码。

这是真实生活吗 ?

这完全是幻想,绝对不是当前任何提议的一部分,但我确实认为这将是代码注入功能的逻辑演变。

它有点像生锈宏 -除外它不允许将任意语句用作参数-同时(我希望)感觉像是C ++的一部分,而不是另一种具有单独语法的语言。

预处理器肯定看起来像是致命的。 但是您可以做很多事情来减少对它的依赖。 通过提供更好的替代方案,C ++社区可以做很多事情来使宏变得越来越不有用。

这可能需要数十年,但将是值得的。 并不是因为宏从根本上来说是糟糕的,而是因为工具越来越多地被认为是基于语言的判断,生存和消亡。

而且因为我们非常需要更好的工具,所以我们需要尽一切可能减少对预处理器的宿命性依赖。

#undef

From: https://hackernoon.com/undefining-the-c-pre-processor-c4eeb3d06e1f

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值