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

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

本文关键字:排序 枚举 编译      更新时间:2023-10-16

我很难理解编译时计算中的一些规则。在这里,我编写了一些代码,将一个唯一的ID与每个请求一个ID的类相关联(以及用于测试目的的解映射名称)。然而,这个唯一的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对一个类列表进行排序,然后包装每个类并为包装器请求ID,则ID将取决于排序;然后要确定"最后一个"元素,您必须手动对元素进行排序!我错过了什么?

这是一个非常有趣的问题,因为它不仅与在C++编译时实现计数器有关,还与在编译时将(静态)计数器值与类型关联有关。

所以我研究了一下,发现了一篇非常有趣的博客文章Filip Roséen 如何在C++中实现常量表达式计数器

他的计数器的实现确实扩展了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成功。这篇博客文章很好地解释了发生的事情

限制

(从文章中提起)

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

将计数器转换为与类型相关的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]:

文本类型的静态数据成员可以在使用constexpr说明符的类定义;如果是,其声明应指定大括号或相等的初始值设定项其中作为赋值表达式的每个初始值设定项子句都是常量表示[注:在两者中在这些情况下,成员可能出现在常量表达式中。——尾注]

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

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如何保持为1,而char将计数器为CCD_ 8。

Live演示

此代码存在与以前相同的缺点

备注

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

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

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

但首先,让我们只使用C++来解决尽可能多的这个问题。我们将使用奇怪的递归模板模式:

template<typename C> class UID {
public:
    static const int id;
};

然后,每个请求唯一ID的类将相应地从该模板继承,从而产生一个名为id:的成员

class Widget : public UID<Widget> {
// ...
};

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

现在,我们所需要做的就是弄清楚如何声明所有类的id值。而且,在这一点上,我们已经达到了C++本身所能做的极限,我们必须呼吁一些增援。

我们将从创建一个文件开始,该文件列出了具有指定ID的所有类。这并不复杂,只是一个名为classlist的简单文件,其内容如下。

Button
Field
Widget

(Button、Field和Widget是继承自UID类的其他类)。

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

1) 一个简单的shell或Perl脚本,它读取classlist文件,并喷出机器人生成的表单代码(给定上面的输入):

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

等等

2) 对您的构建脚本或Makefile进行适当的调整,以编译此机器人生成的代码(包括所有必要的#include等),并将其与您的应用程序链接。因此,想要为其分配ID的类必须显式继承UID类,并将其名称添加到文件中。然后,构建脚本/Makefile会自动运行一个脚本,该脚本会生成一个新的uid列表,并在下一个构建周期中对其进行编译。

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

这只是一个起点。再做一点工作,就可以采用这种基本方法,并将其增强为自动生成constexpruid,这将更好。这将需要解决一些棘手的问题,例如当使用UID的类的列表发生变化时,试图避免触发整个应用程序的重新编译。但是,我认为这也是一个可以解决的问题。。。

Postscriptum:

通过利用编译器特定的扩展,只使用C++仍然可以实现这一点。例如,使用gcc的__COUNTER__宏。