回顾C ++编译时间计数器


28

TL; DR

在尝试阅读整篇文章之前,请了解:

  1. 我自己已经找到解决所提出问题的方法,但是我仍然很想知道分析是否正确;
  2. 我已经将解决​​方案打包到一个fameta::counter类中,该类可以解决一些剩余的怪癖。您可以在github上找到它
  3. 您可以在使用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 ++而言,问题似乎在于,一旦编译器在首次调用函数时评估函数模板的默认参数,则随后对这些函数不会触发默认参数的重新评估,因此从不实例化新函数,而是始终引用先前实例化的函数。


第一个问题

  1. 您实际上是否同意我的这种诊断?
  2. 如果是,该新行为是否由标准强制规定?前一个是错误吗?
  3. 如果没有,那是什么问题?

牢记以上几点,我想出了一个解决方法: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;

您可以在Godbolt上自己查看所有内容。屏幕截图如下。

clang ++认为它已经定义了一些东西,它认为还没有定义

因此,似乎clang ++相信它已经定义了一些东西,认为它尚未定义,是哪种使您的头旋转,不是吗?


第二批问题

  1. 我的解决方法是完全合法的C ++,还是设法发现了另一个g ++错误?
  2. 如果合法,我是否发现了一些讨厌的clang ++错误?
  3. 还是我只是钻研了未定义行为的黑暗黑社会,所以我本人是唯一应受之责的人?

无论如何,我将热烈欢迎任何想帮助我摆脱困境的人,并在必要时提供令人头痛的解释。:D



2
我记得标准委员会的人们明确打算禁止每次(假设)评估时都不会产生完全相同结果的任何类型,形状或形式的编译时构造。因此,这可能是编译器错误,可能是“格式错误,无需诊断”的情况,或者可能是标准遗漏的情况。然而,这违背了“标准精神”。对不起。我也希望编译时间计数器。
bolov

@HolyBlackCat我必须承认,我很难理解该代码。看起来确实可以避免需要将单调递增的数字作为参数传递给next()函数,但是我真的无法弄清楚它是如何工作的。无论如何,我想在这里解决我自己的问题:stackoverflow.com/a/60096865/566849
Fabio A.

@FabioA。我也不太了解这个答案。自问了这个问题之后,我意识到我不想再接触constexpr计数器了。
HolyBlackCat

尽管这是一个有趣的小实验,但是实际上使用该代码的人几乎必须期望它在将来的C ++版本中不起作用,对吗?从这个意义上讲,结果将自身定义为错误。
阿齐斯

Answers:


5

经过进一步的调查,结果发现可以对该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>

尤里卡!

不过,这会破坏clang ++版本<= 7.0.0。

结论

在我看来,我发布的原始解决方案存在概念上的错误,例如:

  • g ++具有怪癖/错误/放松,可以解决我的解决方案的错误并最终使代码正常工作。
  • clang ++版本> 7.0.0更加严格,并且不喜欢原始代码中的错误。
  • 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。当使用ICC编译器不会触发SFINAE decltype(counter(slot<N>())),宁愿抱怨不能够deduce the return type of function "counter(slot<N>)"因为it has not been defined。我相信这是一个错误,可以通过对SFM的直接结果进行SFINAE来解决counter(slot<N>)。这同样适用于所有其他编译器,但是g ++决定发出大量无法关闭的非常烦人的警告。因此,在这种情况下,#ifdef也可以进行救援。

证明上godbolt,下面screnshotted。

在此处输入图片说明


2
我认为这种回答很接近本主题,但是我仍然想知道我的分析是否正确,因此我将等待接受自己正确的答案,希望其他人能够绕过并给我更好的提示或确认。:)
Fabio A.
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.