【问题标题】:C++ compile time counters, revisitedC++ 编译时间计数器,重新审视
【发布时间】:2020-05-21 17:58:54
【问题描述】:

TL;DR

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

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

一切是如何开始的

自从 Filip Roséen 在 2015 年发现/发明了 compile time counters via friend injection are in C++ 的黑魔法后,我一直对这个设备有点着迷,所以当 CWG decided that functionality had to go 我很失望,但仍然希望他们的想法可以改变通过向他们展示一些引人注目的用例。

然后,几年前,我决定再看一遍,以便 uberswitches 可以嵌套 - 在我看来,这是一个有趣的用例 - 只是发现它不再工作 与可用编译器的新版本,即使 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++ 的问题似乎是,一旦编译器在函数第一次评估函数模板默认参数时调用后,对这些函数的任何后续调用都不会触发对默认参数的重新评估,因此永远不会实例化新函数,而是始终引用先前实例化的函数。


第一个问题

  1. 你真的同意我的这个诊断吗?
  2. 如果是,这种新行为是标准规定的吗?上一个是错误吗?
  3. 如果不是,那是什么问题?

牢记上述内容,我想出了一个解决方法:使用单调递增的唯一 id 标记每个 next() 调用,以传递给被调用者,这样就没有相同的调用,因此强制编译器每次重新评估所有参数。

这样做似乎是一种负担,但考虑到这一点,人们可以只使用标准的 __LINE____COUNTER__-like(如果可用)宏,隐藏在 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上观察上面的结果,我已经为懒人截屏了。

如您所见,使用trunk g++ 和 clang++ 直到 7.0.0 都可以使用!,计数器按预期从 0 增加到 3,但 使用高于 7.0.0 的 clang++ 版本它没有

为了雪上加霜,我实际上已经设法使 clang++ 到 7.0.0 版本崩溃,只需在混合中添加一个“上下文”参数,以便计数器实际上绑定到该上下文,并且,这样,可以在定义新上下文时随时重新启动,这为使用可能无限数量的计数器开辟了可能性。使用此变体,7.0.0 以上版本的 clang++ 不会崩溃,但仍不会产生预期的结果。 Live on godbolt.

在不知道发生了什么的情况下,我发现了cppinsights.io 网站,它可以让人们看到模板是如何以及何时被实例化的。使用 service 我认为正在发生的事情是当 writer&lt;N, I&gt; 被实例化时,clang++ 实际上定义了任何 friend constexpr auto counter(slot&lt;N&gt;) 函数。

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

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

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

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

您可以亲自查看所有内容on godbolt。截图如下。

那么,clang++ 似乎认为它已经定义了一些它认为它没有定义的东西,这让你头晕目眩,不是吗?


第二批题

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

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

【问题讨论】:

  • 我记得标准委员会的人们有明确的意图,即禁止任何类型、形状或形式的编译时构造,这些构造在每次(假设)评估时都不会产生完全相同的结果。所以它可能是一个编译器错误,它可能是一个“格式错误,不需要诊断”的案例,或者它可能是标准遗漏的东西。然而,它违背了“标准精神”。我很抱歉。我也会喜欢编译时间计数器。
  • @HolyBlackCat 我必须承认,我发现很难理解该代码。看起来它确实可以避免将单调递增的数字作为参数显式传递给next() 函数的需要,但是我真的不知道它是如何工作的。无论如何,我已经对自己的问题做出了回应,在这里:stackoverflow.com/a/60096865/566849
  • @FabioA。我也不完全理解这个答案。自从问了这个问题后,我意识到我再也不想碰 constexpr 计数器了。
  • 虽然这是一个有趣的小思想实验,但实际使用过该代码的人几乎必须预料到它在未来的 C++ 版本中不会工作,对吧?从这个意义上说,结果将自己定义为一个错误。

标签: c++ counter constexpr argument-dependent-lookup friend-function


【解决方案1】:

经过进一步调查,发现可以对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&lt;N&gt;关联的值,将其加1,然后将这个新值关联到非常同样 slot&lt;N&gt;

slot&lt;N&gt; 没有关联值时,取而代之的是检索与slot&lt;Y&gt; 关联的值,Y 是小于N 的最高索引,因此slot&lt;Y&gt; 具有关联值。

上述代码的问题在于,即使它在 g++ 上工作,clang++(我会说是对的?)使reader(0, slot&lt;N&gt;()) 永久 返回它返回的任何内容,而 slot&lt;N&gt; 没有关联值。反过来,这意味着所有插槽都与基值 0 有效关联。

解决办法是把上面的代码改成这个:

template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N-1>())+1>::value) {
    return R;
}

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

尤里卡!

不过,这会破坏 clang++ 版本

结论

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

  • g++ 有怪癖/错误/放松,它抵消了我的解决方案的错误并最终使代码仍然有效。
  • clang++ 版本 > 7.0.0 更严格,不喜欢原始代码中的错误。
  • clang++ 版本

总结一下,以下代码适用于所有版本的 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&lt;N&gt;())) 时不会触发SFINAE,宁愿抱怨不能deduce the return type of function "counter(slot&lt;N&gt;)" 因为it has not been defined。我相信这是一个错误,可以通过对counter(slot&lt;N&gt;) 的直接结果执行 SFINAE 来解决此问题。这也适用于所有其他编译器,但 g++ 决定吐出大量无法关闭的非常烦人的警告。所以,在这种情况下,#ifdef 也可以来救援。

proof is on godbolt,截图如下。

【讨论】:

  • 我认为这种回答有点结束了话题,但我仍然想知道我的分析是否正确,因此我会等待我自己的回答是正确的,希望其他人会路过,给我一个更好的线索或确认。 :)
猜你喜欢
  • 2015-01-14
  • 1970-01-01
  • 2012-08-19
  • 1970-01-01
  • 2011-09-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多