TL; DR
在尝试阅读整篇文章之前,请了解:
fameta::counter
类中,该类可以解决一些剩余的问题。你可以find it on github; 一切如何开始
自从FilipRoséen在2015年发现/发明了compile time counters via friend injection are in C++这个黑魔法之后,我就对设备有些痴迷,因此当CWG decided that functionality had to go我感到失望时,但仍然希望通过展示一些引人注目的用法可以改变他们的想法案件。
然后,几年前,我决定再次查看一下内容,以便将uberswitch es嵌套(在我看来,这是一个有趣的用例),但发现不再适用于新的版本的可用编译器,即使issue 2118(并且仍然是)处于打开状态:代码可以编译,但计数器不会增加。
已在on Roséen's website中报告了该问题,最近也在stackoverflow上报告了该问题:Does C++ support compile-time counters?
几天前,我决定再次尝试解决问题
我想了解编译器中发生了什么变化,这些变化使看似仍然有效的C++不再起作用。为此,我在互联网上进行了广泛的搜索,以寻找有人对此进行讨论,但无济于事。因此,我已经开始进行实验并得出一些结论,我在这里发表演讲的目的是希望从这里得到比我自己更了解的反馈。
为了清楚起见,下面我将介绍Roséen的原始代码。有关其工作原理的说明,请refer to his website:
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");
}
对于最近使用过的g++和clang++编译器,next()
始终返回1。经过一些试验,至少对于g++而言,问题似乎在于,一旦编译器在首次调用函数时评估函数模板的默认参数,则随后进行任何调用这些函数不会触发对默认参数的重新评估,因此从不实例化新函数,而是始终引用先前实例化的函数。第一个问题
牢记以上几点,我想出了一个解决方法:用单调递增的唯一ID标记每个
next()
调用,以传递给被调用者,这样没有调用是相同的,因此迫使编译器重新评估所有每次争论。这样做似乎很麻烦,但考虑到这一点,您只能使用隐藏在类似于
__LINE__
函数的宏中的标准__COUNTER__
或类似counter_next()
的宏(如果可用)。因此,我提出了以下内容,我以最简化的形式进行了介绍,以显示稍后将要讨论的问题。
template <int N>
struct slot;
template <int N>
struct slot {
friend constexpr auto counter(slot<N>);
};
template <>
struct slot<0> {
friend constexpr auto counter(slot<0>) {
return 0;
}
};
template <int N, int I>
struct writer {
friend constexpr auto counter(slot<N>) {
return I;
}
static constexpr int value = I-1;
};
template <int N, typename = decltype(counter(slot<N>()))>
constexpr int reader(int, slot<N>, int R = counter(slot<N>())) {
return R;
};
template <int N>
constexpr int reader(float, slot<N>, int R = reader(0, slot<N-1>())) {
return R;
};
template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N>())+1>::value) {
return R;
}
int a = next<11>();
int b = next<34>();
int c = next<57>();
int d = next<80>();
您可以在godbolt上观察到上面的结果,我已经为lazies截图了。正如您所看到的,Trunkt g++和clang++的可以工作到7.0.0! ,计数器会按预期从0增加到3,但是langt_rstrong而不是7.0.0以上的clang++版本的。
为了增加侮辱性伤害,我实际上设法使clang++崩溃到了7.0.0版,方法是简单地在混合中添加一个“context”参数,从而使计数器实际上绑定(bind)到该上下文,因此可以可以在定义新上下文的任何时候重新启动,从而可以使用无限数量的计数器。使用此变体,版本7.0.0以上的clang++不会崩溃,但仍无法产生预期的结果。 Live on godbolt。
不知所措,我发现了cppinsights.io网站,该网站可以让人们看到如何以及何时实例化模板。我认为使用service发生的是,每当实例化
friend constexpr auto counter(slot<N>)
时,clang++ 并不实际上定义任何writer<N, I>
函数。尝试显式地为应该已经实例化的任何给定N调用
counter(slot<N>)
似乎为该假设奠定了基础。但是,如果我尝试为应该已实例化的任何给定
writer<N, I>
和N
显式实例化I
,则clang++会提示重新定义了friend constexpr auto counter(slot<N>)
。为了测试以上内容,我在前面的源代码中又添加了两行。
int test1 = counter(slot<11>());
int test2 = writer<11,0>::value;
您可以自己查看所有内容on godbolt。屏幕截图如下。因此,似乎 clang++相信它定义了一些东西,认为它没有定义,这使您的头旋转,不是吗?
第二批问题
无论如何,我都会热烈欢迎任何想帮助我摆脱困境的人,并在必要时提供令人头痛的解释。 :D
最佳答案
经过进一步调查,结果发现可以对next()
函数进行较小的修改,这使代码可以在高于7.0.0的clang++版本上正常工作,但使它对于所有其他clang++版本都停止工作。
看一下下面的代码,这些代码取自我以前的解决方案。
template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N>())+1>::value) {
return R;
}
如果您注意它,从字面意义上讲所做的就是尝试读取与
slot<N>
关联的值,将其添加1,然后将此新值关联到相同的 slot<N>
。当
slot<N>
没有关联值时,取而代之的是获取与slot<Y>
相关的值,且Y
是小于N
的最高索引,因此slot<Y>
具有关联的值。上面的代码的问题在于,即使clang++可以在g++上运行,它也可以使
reader(0, slot<N>())
永久地变为,而返回slot<N>
没有关联值时返回的内容。反过来,这意味着所有插槽都与基本值0
有效关联。解决方案是将以上代码转换为以下代码:
template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N-1>())+1>::value) {
return R;
}
请注意,
slot<N>()
已被修改为slot<N-1>()
。这是有道理的:如果我想将值与slot<N>
关联,则意味着尚未关联任何值,因此尝试检索它没有任何意义。另外,我们要增加一个计数器,并且与slot<N>
关联的计数器的值必须为1加上与slot<N-1>
关联的值。Eureka !
不过,这会破坏clang++版本<= 7.0.0。
结论
在我看来,我发布的原始解决方案存在概念上的错误,例如:
综上所述,以下代码适用于所有版本的g++和clang++。
#if !defined(__clang_major__) || __clang_major__ > 7
template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N-1>())+1>::value) {
return R;
}
#else
template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N>())+1>::value) {
return R;
}
#endif
该代码原样也可用于msvc。
当使用
decltype(counter(slot<N>()))
时,icc编译器不会触发SFINAE,而是提示因为deduce the return type of function "counter(slot<N>)"
而无法使用it has not been defined
。我相信这是一个错误,可以通过对counter(slot<N>)
的直接结果进行SFINAE来解决。这同样适用于所有其他编译器,但是g++决定发出大量无法关闭的非常烦人的警告。因此,同样在这种情况下,#ifdef
可以解决。proof is on godbolt,在下面截图。
关于c++ - 回顾C++编译时间计数器,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60082260/