C++ 编译时间计数器,重温

Fab*_* A. 32 c++ counter friend-function argument-dependent-lookup constexpr

TL; 博士

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

  1. 我自己已经找到了解决所提出问题的方法,但我仍然很想知道分析是否正确;
  2. 我已将解决方案打包到一个fameta::counter类中,以解决一些剩余的怪癖。你可以在 github 上找到它
  3. 你可以在 Godbolt 上看到它。

一切是如何开始的

自从 Filip Roséen 在 2015 年发现/发明了通过友元注入编译时间计数器的黑魔法是在 C++ 中,我一直对这个设备有点着迷,所以当 CWG决定功能必须去时,我很失望,但仍然充满希望可以通过向他们展示一些引人注目的用例来改变他们的想法。

然后,几年前,我决定再看一遍,以便uberswitch es可以嵌套 - 在我看来,这是一个有趣的用例 - 只是发现它不再适用于新版本的可用的编译器,即使问题 2118是(现在仍然是)处于打开状态:代码会编译,但计数器不会增加。

该问题已在 Roséen 的网站和最近的 stackoverflow 上报告:Does C++ support compile-time counters?

几天前,我决定再次尝试解决这些问题

我想了解编译器发生了什么变化,使看似仍然有效的 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");
}
Run Code Online (Sandbox Code Playgroud)

使用 g++ 和 clang++ 最近的编译器,next()总是返回 1。经过一些试验,至少 g++ 的问题似乎是,一旦编译器在第一次调用函数时评估函数模板默认参数,任何后续调用这些函数不会触发对默认参数的重新评估,因此永远不会实例化新函数,而是始终引用先前实例化的函数。


第一个问题

  1. 你真的同意我的这个诊断吗?
  2. 如果是,标准是否强制要求这种新行为?上一个是bug吗?
  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>();
Run Code Online (Sandbox Code Playgroud)

你可以在Godbolt上观察上面的结果,我为懒人截了图。

在此处输入图片说明

正如你所看到的,使用主干 g++ 和 clang++ 直到 7.0.0 它都可以工作!,计数器按预期从 0 增加到 3,但在 7.0.0 以上的 clang++ 版本中,它不会

雪上加霜的是,我实际上已经成功地使 clang++ 崩溃到了 7.0.0 版,只需在混合中添加一个“上下文”参数,这样计数器实际上就绑定到该上下文,因此,可以每当定义新的上下文时都会重新启动,这为使用潜在无限数量的计数器开辟了可能性。使用此变体,7.0.0 以上版本的 clang++ 不会崩溃,但仍不会产生预期的结果。住在 Godbolt 上

在失去任何关于发生了什么的线索的情况下,我发现了cppinsights.io网站,它可以让人们了解模板如何以及何时被实例化。使用该服务,我认为正在发生的事情是,每当实例化时,clang++实际上并没有定义任何friend constexpr auto counter(slot<N>)函数writer<N, I>

试图明确调用counter(slot<N>)任何应该已经实例化的给定 N 似乎为这个假设提供了基础。

但是,如果我尝试显式实例化writer<N, I>任何给定的N并且I应该已经实例化的对象,那么 clang++ 会抱怨重新定义的friend constexpr auto counter(slot<N>).

为了测试上述内容,我在之前的源代码中又添加了两行。

int test1 = counter(slot<11>());
int test2 = writer<11,0>::value;
Run Code Online (Sandbox Code Playgroud)

你可以在 Godbolt 上亲眼看到这一切。截图如下。

clang++ 相信它已经定义了一些它认为没有定义的东西

所以,看起来clang++ 相信它已经定义了一些它认为没有定义的东西,哪种让你头晕目眩,不是吗?


第二批问题

  1. 我的解决方法是合法的 C++,还是我刚刚发现了另一个 g++ 错误?
  2. 如果这是合法的,我是否因此发现了一些讨厌的 clang++ 错误?
  3. 还是我只是深入了未定义行为的黑暗黑社会,所以我自己是唯一的罪魁祸首?

无论如何,我会热烈欢迎任何想帮助我走出这个兔子洞的人,如果需要的话,我会给出令人头疼的解释。:D

Fab*_* A. 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;
}
Run Code Online (Sandbox Code Playgroud)

如果您注意它,它实际上所做的就是尝试读取与 关联的值slot<N>,将其加 1,然后将这个新值与相同的 slot<N>.

slot<N>没有相关联的值,与相关联的值slot<Y>来代替检索,与Y为最高指数小于N使得slot<Y>具有相关联的值。

上面代码的问题在于,即使它适用于 g++,clang++(我会说是正确的?)会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;
}
Run Code Online (Sandbox Code Playgroud)

请注意,slot<N>()已修改为slot<N-1>(). 这是有道理的:如果我想将一个值关联到slot<N>,这意味着还没有关联任何值,因此尝试检索它是没有意义的。另外,我们要增加一个计数器,与关联的计数器slot<N>的值必须是与关联的值加一slot<N-1>

尤里卡!

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

结论

在我看来,我发布的原始解决方案有一个概念性错误,例如:

  • g++ 具有 quirk/bug/relaxation,可以抵消我的解决方案的错误,并最终使代码工作。
  • 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
Run Code Online (Sandbox Code Playgroud)

代码原样也适用于 msvc。icc 编译器在使用时不会触发 SFINAE decltype(counter(slot<N>())),宁愿抱怨不能deduce the return type of function "counter(slot<N>)"因为it has not been defined. 我相信这是一个错误,可以通过对counter(slot<N>). 这也适用于所有其他编译器,但 g++ 决定吐出大量无法关闭的非常烦人的警告。因此,在这种情况下,#ifdef也可以进行救援。

证明上godbolt,下面screnshotted。

在此处输入图片说明

  • 我认为这个回答有点结束了这个话题,但我仍然想知道我的分析是否正确,因此我会等待,然后再接受我自己的答案是正确的,希望其他人能路过并给我更好的线索或确认。:) (2认同)