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

具有作用域的自动静态 C++ 计数器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (16投票s)

2014 年 5 月 22 日

CPOL

8分钟阅读

viewsIcon

33279

downloadIcon

699

一个小型、独立的头文件实现,用于编译时使用的静态计数器。

引言

这是一个小型、独立的头文件实现,用于编译时使用的静态计数器。此实现的特点是:

  • __COUNTER__[^] 这个非标准的编译器宏更具可移植性。
  • __COUNTER__ 不同,可以在单个编译单元中拥有多个实例。
  • Boost::Preprocessor[^] 的计数器相比,使用了更自然的语法。

目前,此实现仅适用于小规模使用,每个计数器实例在 50-100 行代码内。它被设计为嵌入到另一个宏中,每次调用外部宏时,计数器都会递增。此实用程序可用于创建唯一的注册 ID 或自动索引条目。

背景

最近我一直在开发一个网络通信库,名为 Alchemy[^]。Alchemy 自动管理网络传输数据的字节序转换。它能够以编程方式检查消息定义并执行正确的转换。此任务通过迭代定义消息的类型数组来完成。我从零开始迭代类型列表。

我在 Davide Di Gennaro 的著作《Advanced C++ Metaprogramming》中看到一个模板元编程的例子,它使用了 __LINE__ 预处理器宏来定义唯一的标识符。他的例子展示了一个从零开始计数并返回模板定义所在行号作为唯一 ID 的模板。我想,如果我能构建一个反向模板,就能少一个用户需要定义的字段。我首先转向互联网,但找不到解决此问题的方法。于是,我着手寻找解决方案,这就是我在这里向大家展示的内容。

Using the Code

这是一组必须在特定作用域内唯一的宏。它们可以用于类定义或命名空间。不幸的是,此版本的计数器无法在函数内部实例化,甚至无法在嵌套类内部实例化。抱歉,这是规则。下面是使用计数器外观的快速演示。

// Basic use declaration.
DECLARE_COUNTER            // Line: Base
INC_COUNTER                // Line: Base + 1
INC_COUNTER                // Line: Base + 2

INC_COUNTER                // Line: Base + 4




INC_COUNTER                // Line: Base + 8

INC_COUNTER                // Line: Base + 10

前面的块创建了一组模板,当传入行号 - 基准号时,它们将返回在某一行声明的计数索引。下面的代码块演示了如何从其声明中提取这些值。

cout << "line(1)  value: " << auto_index<1>::value  << "\n"
     << "line(2)  value: " << auto_index<2>::value  << "\n"
     << "line(4)  value: " << auto_index<4>::value  << "\n"
     << "line(8)  value: " << auto_index<8>::value  << "\n"
     << "line(10) value: " << auto_index<10>::value << "\n";

// The output is:
//
// line(1)  value: 0
// line(2)  value: 1
// line(4)  value: 2
// line(8)  value: 3
// line(10) value: 4
//

这个牵强的例子旨在演示该实用程序的各个组成部分。首先是计数器定义,其次是用于查询索引的 auto_index 模板。前面的块在任何非琐碎的程序中都很难正确使用。这就是为什么这个计数器打算嵌入到用户定义的宏中。这将允许递增调用和索引访问发生在同一行。

下面的块演示了一个更接近静态计数器预期用法的示例。

// Sample Usage MACRO
#define REGISTER_OBJECT(CLASS)                                    \
  INC_COUNTER                                                     \
  static const size_t CLASS::k_uid = auto_index<__LINE__ - k_base>::value;

// Each type of class to be registered needs a unique ID, 
// There is an internal constant named k_uid that holds this id.
DECLARE_COUNTER

REGISTER_OBJECT(ObjectTypeA);
REGISTER_OBJECT(ObjectTypeB);
REGISTER_OBJECT(ObjectTypeC);
REGISTER_OBJECT(ObjectTypeD);

预处理器运行后,__LINE__ 编译器宏的使用将出现在 INC_COUNTER 声明的同一行。当从这个实例化行号中减去基准枚举行号时,将定义一个新值,从零开始。

关注点

首先我想提的是,我最初试图为实现创建一组纯模板定义。然而,有一件事我就是找不到解决方案,那就是模板特化。模板特化有一个限制,它必须在模板定义所在的命名空间或父命名空间中执行。

当我得到一个基本工作的解决方案时,我尝试将计数器应用于我最初创建它的目的。我很快意识到它行不通,因为我的使用需要计数器出现在类作用域中。然后我离开了这个项目几天。

回来后,我意识到如果我将这些模板转换为宏,我就可以在任何我想使用计数器的作用域内重新定义一组这些模板。这实际上是一个更好的解决方案,因为可以在单个编译单元中使用多个计数器。它们只需要放在自己的作用域内。

它是如何工作的?

原则上,这个概念非常简单。对于定义的每个 auto_index 模板实例

  • 创建计数器的新实例时,实例化一个模板,将该行标记为索引条目。
  • 对于新行的声明,从前一行开始,一直搜索直到找到最近创建的索引。
  • 基准行及其下方将是下限,并作为向后计数终止条件。

不幸的是,事实证明这是一个细节决定成败的问题。重要的是要记住,一旦为单个计数器实例开始了声明,整个定义和实例化必须随着单个语句的声明而完成。

为什么?

因为这个解决方案的目标是有一个宏,它在每次实例化时都被调用得完全相同。但它仍然必须递增一个值。以下是我在达成解决方案时必须考虑或克服的其他挑战:

  • 解决方案必须在编译时静态解析(为最初预期的用途)。
  • 不能创建状态。一旦定义完成,编译器移动到下一条语句,则无法访问之前的状态。
  • 声明变量或 typedefs 将是一个挑战,因为符号名称将在同一作用域内发生冲突。
  • 薛定谔的模板

量子模板编程

什么是 *量子模板*?这让我绞尽脑汁试图找到解决方案。很早就我设计了向后计数解决方案。我还决定用 true_type 定义的 enum_entry 模板来标记有效行。这将使未定义定义的行在查询 enum_entry 以查看当前行是否定义了索引时返回 false_type

这时我看到了模板形式的薛定谔的猫。我试图发现一个模板定义是否有效。然而,当我查询该模板的那一刻,我就已经定义了它。如果我什么都不做,它就不会存在。这就是模板特化变得必要的原因。

我曾设想一个解决方案,我只需定义一个用于递增计数器的行的模板。然而,根本无法通用地定义一个有效行和一个无效行。为了打破这个令人抓狂的循环,我不得不转向特化。

这是默认模板定义。此声明默认将该条目标记为无效枚举。

  template<int N, bool IsValidT = false>
  struct enum_entry
    : std::integral_constant<bool, IsValidT>
  { };

这是为每一行递增计数器而定义的特化模板。

  template < >
  struct enum_entry< (ID) >
    : std::true_type
  { };

工作原理

在创建枚举条目特化后,必须调用 auto_index < int L < 才能按定义行的顺序以递增顺序分配计数器值。这启动了递归模板处理。

  template <int L>
  struct auto_index
    : std::integral_constant< int, index_before<L>::value + 1 >
  { };

模板定义为创建一个简单的静态常量,其中计数器的当前值将设置为最后一个值加一。

  std::integral_constant< int, index_before<L>::value + 1 >

index_before 是一个方便的模板,用于简化对上一个索引的第一次请求。这启动了搜索,从上一行开始,一次向前计数一个,直到找到最后一个有效的 enum_entry 模板。

  template <int L>
  struct decrement_until_match
    : std::integral_constant< int,
                              value_if< enum_entry<L>::value,
                                        int,
                                        auto_index<L>::value,
                                        decrement_until_match<L - 1>::value
                                      >::value
                            >
  { };

让我们放大并仔细看看决策点。

  value_if< enum_entry<L>::value,                // Predicate
            int,
            auto_index<L>::value,                // Previous Index Found
            decrement_until_match<L - 1>::value  // This is not a valid line,
          >::value                               // search the previous line.

value_if 是我编写的一个条件模板,用于简化进行布尔决策,然后根据结果分配值的语法 TRUE | FALSE 。它的行为相当于元编程的三元运算符 ?:。实现很简单。模板有两个定义;一个用于 true 情况,一个用于 false 情况。

  //  ***************************************************************************
  /// This construct provides the compile-time equivalent of 
  /// the ternary operator ?: for value selection.
  ///
  template <bool predicate, typename T, T trueValue, T falseValue>
  struct value_if
    : std::integral_constant<T, trueValue>
  { };

  //  ***************************************************************************
  /// Specialization for the false condition of the value selector.
  ///
  template <typename T, T trueValue, T falseValue>
  struct value_if<false, T, trueValue, falseValue>
    : std::integral_constant<T, falseValue>
  { };

最后,有一个终止的 enum_entry 特化,以及一个充当第一个索引的种子值的模板。

  // Terminating enum_entry. Do not search before line 0.
  template<>
  struct enum_entry<0, true>
    : std::true_type
  { };

  // Initial auto_index specialization to 
  // start the first index at 0.
  template <>
  struct auto_index<0>
    : std::integral_constant< int, -1>
  { };

限制

因为 INC_COUNTER 宏依赖于 __LINE__ 编译器宏,所以每个递增定义必须定义在不同的行上。这也意味着 INC_COUNTER 不能在同一个多行宏中出现多次。INC_COUNTER 调用不需要按顺序出现。这是因为它是通过搜索最后声明的索引来查找,而不是简单的基于基数的偏移计算。

由于实现依赖于递归模板实例化来计算下一个索引,因此存在一个依赖于编译器嵌套深度的限制。一组计数宏的声明也必须相对靠近。因此,我建议您将这些宏嵌入到其他宏中,并创建一个包含定义的声明块。

也许可以通过修改计数循环来添加条件语句,使奇数走一条路,偶数走另一条路。这实际上会使深度加倍。我没有探索这些选项,因为我提出的方案目前满足了我的需求。而且,我也不想把已经很复杂的东西弄得更复杂。

© . All rights reserved.