TL; DR
在尝试阅读整篇文章之前,请了解:
- 我自己已经找到解决所提出问题的方法,但是我仍然很想知道分析是否正确;
- 我已经将解决方案打包到一个
fameta::counter
类中,该类可以解决一些剩余的怪癖。您可以在github上找到它; - 您可以在使用Godbolt上看到它。
一切如何开始
自从FilipRoséen在2015年发现/发明以来,编译计时器的黑魔法就用C ++了,我一直对这个设备有些痴迷,所以当CWG 决定必须取消功能时,我很失望,但仍然希望他们的想法通过向他们展示一些引人注目的用例可以对其进行更改。
然后,几年前,我决定再次研究一下问题,以便将uberswitch es嵌套(在我看来,这是一个有趣的用例),只是发现它不再适用于新版本的。可用的编译器,即使问题2118处于(并且仍然)处于打开状态:代码可以编译,但计数器不会增加。
在Roséen的网站上已经报告了该问题,最近在stackoverflow上也报告了该问题:C ++是否支持编译时计数器?
几天前,我决定再次尝试解决问题
我想了解编译器中发生了什么变化,这些变化使看似仍然有效的C ++不再起作用。为此,我在互联网上进行了广泛的搜索,寻找有人对此进行讨论,但无济于事。因此,我开始进行实验并得出一些结论,我在这里提出的想法是希望从这里得到比我自己更了解的反馈。
为了清楚起见,下面我将介绍Roséen的原始代码。有关其工作原理的说明,请访问他的网站:
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 ++而言,问题似乎在于,一旦编译器在首次调用函数时评估函数模板的默认参数,则随后对这些函数不会触发默认参数的重新评估,因此从不实例化新函数,而是始终引用先前实例化的函数。
第一个问题
- 您实际上是否同意我的这种诊断?
- 如果是,该新行为是否由标准强制规定?前一个是错误吗?
- 如果没有,那是什么问题?
牢记以上几点,我想出了一个解决方法:next()
用单调递增的唯一ID 标记每个调用,以传递给被调用者,这样就不会有相同的调用,因此迫使编译器重新评估所有参数。每一次。
这样做似乎是一种负担,但考虑到这一点,您可以只使用隐藏在类似函数的宏中的标准__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截图了。
正如您所看到的,直到7.0.0之前,它都可以使用主干g ++和clang ++!,计数器会如预期的那样从0增加到3,但是当clang ++版本高于7.0.0时,计数器不会增加。
为了增加侮辱性伤害,我实际上已经设法使clang ++崩溃到7.0.0版,方法是简单地在混合中添加“ context”参数,这样计数器实际上就绑定到了该上下文,因此,可以在定义新上下文的任何时候重新启动,从而可以使用可能无限数量的计数器。使用此变体,版本7.0.0以上的clang ++不会崩溃,但仍无法产生预期的结果。活在当下。
不知所措,我发现了cppinsights.io网站,该网站可以让人们看到如何以及何时实例化模板。使用该服务,我认为正在发生的情况是,在实例化时,clang ++ 实际上并未定义任何friend constexpr auto counter(slot<N>)
功能writer<N, I>
。
试图显式地调用counter(slot<N>)
应该已经实例化的任何给定N似乎为该假设奠定了基础。
但是,如果我尝试writer<N, I>
为任何给定显式实例化N
并且I
应该已经实例化,则clang ++会抱怨redefined friend constexpr auto counter(slot<N>)
。
为了测试以上内容,我在前面的源代码中又添加了两行。
int test1 = counter(slot<11>());
int test2 = writer<11,0>::value;
因此,似乎clang ++相信它已经定义了一些东西,认为它尚未定义,是哪种使您的头旋转,不是吗?
第二批问题
- 我的解决方法是完全合法的C ++,还是设法发现了另一个g ++错误?
- 如果合法,我是否发现了一些讨厌的clang ++错误?
- 还是我只是钻研了未定义行为的黑暗黑社会,所以我本人是唯一应受之责的人?
无论如何,我将热烈欢迎任何想帮助我摆脱困境的人,并在必要时提供令人头痛的解释。:D
next()
函数,但是我真的无法弄清楚它是如何工作的。无论如何,我想在这里解决我自己的问题:stackoverflow.com/a/60096865/566849