类如何在编译时进行枚举、排序等?

How can classes be enumerated, ordered, etc. at compile time?

我正在努力解决一些可以推入编译时计算的规则。在这里,我编写了代码,将唯一 ID 与请求一个的每个 class 相关联(以及用于测试目的的 demangled 名称。)但是,此唯一 ID 不能用作模板参数或模板参数的一部分static_assert 条件,因为它不是一个 constexpr。

#include <cassert>
#include <cxxabi.h>
#include <iostream>
#include <typeinfo>

namespace UID {
    static int nextID(void) {
        static int stored = 0;
        return stored++;
    }
    template<class C>
    static int getID(void) {
        static int once = nextID();
        return once;
    }
    template<class C>
    static const char *getName(void) {
        static int status = -4;
        static const char *output =
            abi::__cxa_demangle(typeid(C).name(), 0, 0, &status);
        return output;
    }
}

namespace Print {
    template<class C>
    std::ostream& all(std::ostream& out) {
        return out << "[" << UID::getID<C>() << "] = "
            << UID::getName<C>() << std::endl;
    }
    template<class C0, class C1, class... C_N>
        std::ostream& all(std::ostream& out) {
        return all<C1, C_N>(all<C0>(out));
    }
}

void test(void) {
    Print::all<int, char, const char*>(std::cout) << std::endl;
    // [0] = int
    // [1] = char
    // [2] = char const*
    Print::all<char, int, const char*>(std::cout);
    // [1] = char
    // [0] = int
    // [2] = char const*
}

如果不清楚,我想根据 ID 更改其他编译时行为。我见过几种涉及类型链接列表的方法,因此 ID 是先前分配的 constexpr ID 和 constexpr 偏移量的总和。但是,我看不出这比手动分配 ID 有何改进。如果您要按 ID 对 classes 的列表进行排序,然后包装每个 classes 并为包装器请求 ID,则 ID 将取决于排序;那么要确定 "last" 元素,您将不得不手动对元素进行排序!我错过了什么?

有时,不得不承认 C++ 本身并不能解决世界上所有的问题。

有时,有必要将额外的工具和脚本集成到一个人的构建系统中。我认为这是其中一种情况。

但首先,让我们只使用 C++ 来尽可能多地解决这个问题。我们将使用 Curiously Recursive Template Pattern:

template<typename C> class UID {

public:

    static const int id;
};

然后,每个请求唯一 ID 的 class 将从该模板继承,因此,会产生一个名为 id:

的成员
class Widget : public UID<Widget> {

// ...

};

因此,Widget::id 成为 class 的唯一 ID。

现在,我们需要做的就是找出如何声明所有 classes 的 id 值。而且,在这一点上,我们已经达到了 C++ 自身所能做的极限,我们必须调用一些增援。

我们将首先创建一个文件,其中列出所有 class 具有指定 ID 的元素。这并不复杂,只是一个简单的文件,例如,名为 classlist,其内容就是这样。

Button
Field
Widget

(按钮、字段和小部件不是从 UID class 继承的 class)。

现在,它变成了一个简单的两步过程:

1) 一个简单的 shell 或 Perl 脚本,它读取 classlist 文件,并输出 robo-generated 形式的代码(给定上述输入):

const int UID<Button>::id=0;
const int UID<Field>::id=1;
const int UID<Widget>::id=2;

...等等。

2) 对您的构建脚本或 Makefile 进行适当的调整,以编译此 robo-generated 代码(使用所有必要的 #include,等等...,以实现此目的),并 link 它与您的应用程序。因此,想要为其分配 ID 的 class 必须显式继承 UID class,并将其名称添加到文件中。然后构建 script/Makefile 会自动运行生成新 uid 列表的脚本,并在下一个构建周期编译它。

(希望您使用真正的C++开发环境,它为您提供了灵活的开发工具,而不是被迫遭受一些不灵活的visual-IDE类型限制开发环境,功能有限)。

这只是一个起点。再多做一点工作,应该可以采用这种基本方法,并将其增强为 auto-generate constexpr uid,这样会更好。这将需要破解一些棘手的问题,例如当 UID-using classes 列表发生变化时,试图避免触发整个应用程序的重新编译。但是,我认为这也是一个可以解决的问题...

后记:

通过利用 compiler-specific 扩展,仍然可以仅使用 C++ 来完成此操作。例如,使用 gcc's __COUNTER__ macro.

这是一个非常有趣的问题,因为它不仅与在 C++ 中实现 compile-time 的计数器有关,还与将(静态)计数器值与 compile-time.[=25 的类型相关联有关=]

所以我研究了一下,发现了一个非常有趣的博客 post How to implement a constant expression counter in C++ by Filip Roséen

他实现的计数器确实扩展了 ADL 和 SFINAE 的工作范围:

template<int N>
struct flag {
  friend constexpr int adl_flag (flag<N>);
};
template<int N>
struct writer {
  friend constexpr int adl_flag (flag<N>) {
    return N;
  }

  static constexpr int value = N;
};
template<int N, int = adl_flag (flag<N> {})>
int constexpr reader (int, flag<N>) {
  return N;
}

template<int N>
int constexpr reader (float, flag<N>, int R = reader (0, flag<N-1> {})) {
  return R;
}

int constexpr reader (float, flag<0>) {
  return 0;
}
template<int N = 1>
int constexpr next (int R = writer<reader (0, flag<32> {}) + N>::value) {
  return R;
}
int main () {
  constexpr int a = next ();
  constexpr int b = next ();
  constexpr int c = next ();

  static_assert (a == 1 && b == a+1 && c == b+1, "try again");
}

本质上它依赖于 ADL 未能找到 friend 函数的适当定义,导致 SFINAE,并使用模板递归直到完全匹配或 ADL 成功。博客 post 很好地解释了正在发生的事情。

限制

(摘自文章)

  • 您不能在多个翻译单元中使用相同的计数器,否则您可能会违反 ODR。
  • 注意 constexpr 生成的值之间的一些比较运算符;尽管调用顺序不同,但有时无法保证编译器实例化它们的相对时间。 (我们可以用 std::atomic 做些什么吗?)
    • 这意味着如果在 compile-time 时评估 a < b 不能保证为真,即使它会在 运行 之前。
  • 模板参数替换顺序;可能导致跨 C++11 编译器的行为不一致;固定在 C++14
  • MSVC 支持:即使 Visual Studio 2015 附带的编译器仍然不完全支持表达式 SFINAE。博客 post.
  • 中提供的解决方法

将计数器变成 type-associated UUID

事实证明,更改起来非常简单:

template<int N = 1, int C = reader (0, flag<32> ())>
int constexpr next (int R = writer<C + N>::value) {
  return R;
}

进入

template<typename T, int N = 1>
struct Generator{
 static constexpr int next = writer<reader (0, flag<32> {}) + N>::value; // 32 implies maximum UUID of 32
};

鉴于 const static int 是您可以声明和定义的少数类型之一 在同一个地方 [9.4.2.3]:

A static data member of literal type can be declared in the class definition with the constexpr specifier; if so, its declaration shall specify a brace-or-equal-initializer in which every initializer-clause that is an assignment-expression is a constant expression. [ Note: In both these cases, the member may appear in constant expressions. — end note ]

所以现在我们可以这样写代码了:

constexpr int a = Generator<int>::next;
constexpr int b = Generator<int>::next;
constexpr int c = Generator<char>::next;

static_assert(a == 1, "try again");
static_assert(b == 1, "try again");
static_assert(c == 2, "try again");

注意 int 如何保持 1char 递增 反对 2.

直播 演示

此代码存在与以前相同的所有缺点 (可能还有更多我没有想到的)

备注

由于每个整数值的 friend constexpr int adl_flag(flag<N>) 声明如此之多,因此此代码会出现大量编译器警告;实际上每个未使用的计数器值一个。