在现代C ++代码中使用X-Macro

现代C ++的发展趋势表明,在代码中最大程度地拒绝了宏。 但是有时没有宏,并且以它们特别丑陋的表现,就无法做,因为没有宏会更糟。 关于这个和故事。

如您所知,编译C和C ++的第一步是预处理器,它用纯文本替换宏和预处理器指令。

这使我们可以做一些奇怪的事情,例如:

// xmacro.h "look, I'm a string!" // xmacro.cpp std::string str = #include "xmacro.h" ; 

在预处理器工作之后,这种误解将变成正确的代码:

 std::string str = "look, I'm a string!" ; 

当然,此可怕的标头不能包含在其他任何地方。 是的,由于我们将多次将此标头添加到同一个文件中,因此没有#pragma一次,也没有包含防护。

实际上,让我们编写一个更复杂的示例,该示例将在宏的帮助下执行不同的操作,同时防止随机出现的#include:

 // xmacro.h #ifndef XMACRO #error "Never include me directly" #endif XMACRO(first) XMACRO(second) #undef XMACRO // xmacro.cpp enum class xenum { #define XMACRO(x) x, #include "xmacro.h" }; std::ostream& operator<<(std::ostream& os, xenum enm) { switch (enm) { #define XMACRO(x) case xenum::x: os << "xenum::" #x; break; #include "xmacro.h" } return os; } 

这仍然很丑陋,但是已经出现了某种魅力:将新元素添加到枚举类时,它将自动添加到重载的输出语句中。

在这里,您可以形式化此方法的应用领域:基于一个来源在不同位置生成代码的需求。

现在是X-Macro和Windows的悲惨故事。 有诸如Windows Performance Counters这样的系统,它使您可以将某些计数器发送到操作系统,以便其他应用程序可以选择它们。 例如,可以将Zabbix配置为收集和监视任何性能计数器。 这非常方便,并且您不需要通过返回/查询数据来重新构造轮子。

我真诚地认为,添加一个新计数器看起来像一个HANDLE计数器= AddCounter(“名称”)。 啊,我错了。

首先,您需要编写一个特殊的XML清单( 示例 ),或者使用Windows SDK中的ecmangen.exe程序生成它,但是由于某种原因,该ecmangen 已从 Windows 10 SDK的新版本中删除 。 接下来,您需要根据我们的XML清单使用ctrpp实用程序生成代码和.rc文件。 仅使用lodctr实用程序将新计数器添加到系统本身,并在参数中包含XML清单。

什么是.rc文件?
这是Microsoft的发明,与标准C ++不相关。 使用这些文件,您可以将资源(例如字符串\图标\图片等)嵌入exe \ dll中,然后使用特殊的Windows API进行提取。

Perfcounters使用这些.rc文件来本地化计数器名称,但不清楚为什么要本地化这些名称。

总结以上内容:要添加1个计数器,您需要:

  1. 更改XML清单
  2. 根据清单生成新的.c和.rc项目文件
  3. 编写一个新函数,将增加一个新计数器
  4. 编写一个将获取计数器值的新函数

总计:diff-e中有4-5个修改过的文件,以便使用一个计数器,并且不断遭受XML清单的影响,而XML清单是plus代码中的信息源。 这就是微软为我们提供的。

实际上,发明的解决方案看起来很吓人,但是在一个文件中恰好在一行中添加了一个新的计数器。 此外,由于仍然需要XML清单,尽管现在还不是主要的清单,但所有内容都是使用宏和不幸的是使用预构建脚本自动生成的。

我们的perfcounters_ctr.h看起来与上面的示例几乎相同:

 #ifndef NV_PERFCOUNTER #error "You cannot do this!" #endif ... NV_PERFCOUNTER(copied_bytes) NV_PERFCOUNTER(copied_files) ... #undef NV_PERFCOUNTER 

如我先前所写,添加计数器是通过使用lodctr.exe加载XML清单来完成的。 在我们的程序中,我们只能对其进行初始化和修改。

我们在生成的打开器中感兴趣的初始化片段如下所示:

 #define COPIED_BYTES 0 //     0 #define COPIED_FILES 1 //      const PERF_COUNTERSET_INFO counterset_info{ ... 2, //    XML-  ... }; struct { PERF_COUNTERSET_INFO set; PERF_COUNTER_INFO counters[2]; //     } counterset { counterset_info, { //     { COPIED_BYTES, ... }, { COPIED_FILES, ... } } } 

总计:我们需要采用“计数器名称-递增索引”形式的对应关系,并且在编译阶段,必须知道计数器的数量并从计数器索引中收集初始化数组。 这是X宏进行救援的地方。

将计数器名称与其上升索引进行匹配非常简单。

下面的代码将变成一个枚举类,其内部索引从0开始,然后递增1。 用手添加最后一个元素,我们立即找出有多少总计数器:

 enum class counter_enum : int { #define NV_PERFCOUNTER(ctr) ctr, #include "perfcounters_ctr.h" total_counters }; 

而且,根据我们的枚举,我们需要初始化计数器:

 static constexpr int counter_count = static_cast<int>(counter_enum::total_counters); const PERF_COUNTERSET_INFO counterset_info{ ... counter_count, ... }; struct { PERF_COUNTERSET_INFO set; PERF_COUNTER_INFO counters[counter_count]; } counterset { counterset_info, { //     #define NV_PERFCOUNTER(ctr) \ { static_cast<int>(counter_enum::ctr), ... }, #include "perfcounters_ctr.h" } } 

结果是,新计数器的初始化现在需要1行,并且不需要其他文件中的其他更改(以前,每次重新生成仅在初始化时更改了3条代码)。

让我们添加一个方便的API来增加计数器。 精神上的东西:

 #define NV_PERFCOUNTER(ctr) \ inline void ctr##_tick(size_t diff = 1) { /*   counter_enum::ctr */ } #include "perfcounters_ctr.h" #define NV_PERFCOUNTER(ctr) \ inline size_t ctr##_get() { /*    counter_enum::ctr */ } #include "perfcounter_ctr.h" 

预处理器将为我们生成漂亮的getter / setter,我们可以立即在代码中使用它们,例如:

 inline void copied_bytes_tick(size_t diff = 1); inline size_t copied_bytes_get(); 

但是,我们仍然有2件令人遗憾的事情:XML清单和.rc文件(可惜,这是必要的)。

我们非常简单-一个预构建脚本,该脚本使用定义计数器的宏读取原始文件,解析“ NV_COUNTER(“和”)”之间的内容,并基于此生成两个.gitignore文件,以便不要乱扔差异。

它是 :基于XML清单的特殊软件生成的编码代码。 每次添加/删除计数器,项目中都会发生很多更改。

现在:预处理程序和预构建脚本生成所有计数器,XML清单和.rc文件。 diff-e中恰好一行用于添加/删除计数器。 感谢预处理器帮助解决了这个问题,在这种特殊情况下,显示出的好处多于伤害。

Source: https://habr.com/ru/post/zh-CN475162/


All Articles