C++ 编译时间计数器,再访

C++ compile time counters, revisited

TL;DR

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

  1. 提出问题的解决方案,但我仍然很想知道分析是否正确;
  2. 我已将解决方案打包成 fameta::counter class 解决了一些剩余的怪癖。你可以 find it on github;
  3. 你可以在work on godbolt看到它。

这一切是如何开始的

自 2015 年 Filip Roséen discovered/invented 以来,黑魔法 compile time counters via friend injection are in C++, I have been mildly obsessed with the device, so when the CWG decided that functionality had to go 我很失望,但仍然希望通过向他们展示一些引人注目的用例来改变他们的想法。

然后,几年前我决定再看看这东西,所以 uberswitches could be nested - an interesting use case, in my opinion - only to discover that it wouldn't work any longer with the new versions of the available compilers, even though issue 2118 是(并且 仍然是 )处于打开状态:代码可以编译,但计数器不会增加。

问题已报告on Roséen's website and recently also on Whosebug: 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() 总是 returns 1. 经过一些试验,至少 g++ 的问题似乎是,一旦编译器评估了函数模板第一次调用函数时的默认参数,对这些函数的任何后续调用都不会触发对默认参数的重新评估,因此永远不会实例化新函数,而是始终引用先前实例化的函数。


第一题

  1. 你真的同意我的这个诊断吗?
  2. 如果是,这个新行为是标准强制要求的吗?上一个是bug吗?
  3. 如果不是,那是什么问题?

考虑到上述情况,我想出了一个解决方法:用单调递增的唯一 ID 标记每个 next() 调用,以传递给被调用者,这样就不会有相同的调用,因此强制编译器每次重新评估所有参数。

这样做似乎是一种负担,但考虑到这一点,我们可以只使用隐藏在 counter_next() 中的标准 __LINE__ 或类似 __COUNTER__ 的(只要可用)宏类似函数的宏。

所以我想到了以下内容,我以最简化的形式展示了我稍后将讨论的问题。

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上观察上面的结果,我已经截图给懒人了。

如您所见,在 7.0.0 之前使用 trunk g++ 和 clang++ 可以正常工作!,计数器按预期从 0 增加到 3,但是 对于高于 7.0.0 的 clang++ 版本,它不会.

雪上加霜的是,我实际上设法让 clang++ 在 7.0.0 版之前崩溃,方法是简单地向混合中添加一个“上下文”参数,这样计数器实际上绑定到该上下文,并且因此,可以在定义新上下文时随时重新启动,这为使用潜在无限数量的计数器提供了可能性。使用此变体,7.0.0 版以上的 clang++ 不会崩溃,但仍然不会产生预期的结果。 Live on godbolt.

由于对发生的事情一无所知,我发现 cppinsights.io website, that lets one see how and when templates get instantiated. Using that service 我认为正在发生的事情是 clang++ 没有 实际上定义任何每当实例化 writer<N, I> 时,friend constexpr auto counter(slot<N>) 就会起作用。

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

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

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

int test1 = counter(slot<11>());
int test2 = writer<11,0>::value;

你可以自己看看on godbolt。屏幕截图如下。

所以,看起来 clang++ 认为它定义了一些它认为它没有定义的东西,这让你头晕,不是吗?


第二批题目

  1. 我的 解决方法 是否完全合法的 C++,还是我设法发现了另一个 g++ 错误?
  2. 如果它是合法的,我是否因此发现了一些讨厌的 clang++ 错误?
  3. 还是我只是深入研究了未定义行为的黑暗地下世界,所以我自己才是唯一的罪魁祸首?

无论如何,我会热烈欢迎任何想帮助我走出这个兔子洞的人,如果需要的话,我会给出令人头疼的解释。 :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> 有关联值。

上面代码的问题在于,即使它在 g++ 上运行,clang++(我会说是正确的吗?)使 reader(0, slot<N>()) 永久 return 当 slot<N> 没有相关值时,无论它 return 是什么。反过来,这意味着所有插槽都与基值 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> 关联的计数器的值必须是与 slot<N-1>.

关联的值的一加

尤里卡!

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

结论

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

  • 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

代码原样也适用于 msvc。 icc 编译器在使用 decltype(counter(slot<N>())) 时不会触发 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,截图如下。