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

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

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

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

  • 一切如何开始
    自从FilipRoséen在2015年发现/发明了compile time counters via friend injection are in C++这个黑魔法之后,我就对设备有些痴迷,因此当CWG decided that functionality had to go我感到失望时,但仍然希望通过展示一些引人注目的用法可以改变他们的想法案件。
    然后,几年前,我决定再次查看一下内容,以便将uberswitch es嵌套(在我看来,这是一个有趣的用例),但发现不再适用于新的版本的可用编译器,即使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++而言,问题似乎在于,一旦编译器在首次调用函数时评估函数模板的默认参数,则随后进行任何调用这些函数不会触发对默认参数的重新评估,因此从不实例化新函数,而是始终引用先前实例化的函数。

    第一个问题
  • 您实际上是否同意我的诊断?
  • 如果是,此新行为是否由标准强制规定?前一个是错误吗?
  • 如果不是,那是什么问题?

  • 牢记以上几点,我想出了一个解决方法:用单调递增的唯一ID标记每个next()调用,以传递给被调用者,这样没有调用是相同的,因此迫使编译器重新评估所有每次争论。
    这样做似乎很麻烦,但考虑到这一点,您只能使用隐藏在类似于__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截图了。
    enter image description here
    正如您所看到的,Trunkt g++和clang++的可以工作到7.0.0! ,计数器会按预期从0增加到3,但是langt_rstrong而不是7.0.0以上的clang++版本的
    为了增加侮辱性伤害,我实际上设法使clang++崩溃到了7.0.0版,方法是简单地在混合中添加一个“context”参数,从而使计数器实际上绑定(bind)到该上下文,因此可以可以在定义新上下文的任何时候重新启动,从而可以使用无限数量的计数器。使用此变体,版本7.0.0以上的clang++不会崩溃,但仍无法产生预期的结果。 Live on godbolt
    不知所措,我发现了cppinsights.io网站,该网站可以让人们看到如何以及何时实例化模板。我认为使用service发生的是,每当实例化friend constexpr auto counter(slot<N>)时,clang++ 并不实际上定义任何writer<N, I>函数。
    尝试显式地为应该已经实例化的任何给定N调用counter(slot<N>)似乎为该假设奠定了基础。
    但是,如果我尝试为应该已实例化的任何给定writer<N, I>N显式实例化I,则clang++会提示重新定义了friend constexpr auto counter(slot<N>)
    为了测试以上内容,我在前面的源代码中又添加了两行。
    int test1 = counter(slot<11>());
    int test2 = writer<11,0>::value;
    
    您可以自己查看所有内容on godbolt。屏幕截图如下。
    clang++ believes it has defined something that it believes it hasn't defined
    因此,似乎 clang++相信它定义了一些东西,认为它没有定义,这使您的头旋转,不是吗?

    第二批问题
  • 我的变通办法是完全合法的C++,还是设法发现了另一个g++错误?
  • 如果合法,我是否发现了一些讨厌的clang++错误?
  • 还是我只是深入研究了未定义行为的黑暗黑社会,所以我本人是唯一应该受到指责的人?

  • 无论如何,我都会热烈欢迎任何想帮助我摆脱困境的人,并在必要时提供令人头痛的解释。 :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>具有关联的值。

    上面的代码的问题在于,即使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>关联的值。

    Eureka !

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

    结论

    在我看来,我发布的原始解决方案存在概念上的错误,例如:
  • g++具有怪癖/错误/放松,可消除解决方案的错误并最终使代码正常工作。
  • > 7.0.0的
  • 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。
    当使用decltype(counter(slot<N>()))时,icc编译器不会触发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,在下面截图。

    enter image description here

    关于c++ - 回顾C++编译时间计数器,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60082260/

    相关文章:

    c++ - 光线追踪/Phong 模型漫反射组件

    c - 字计数器在 C 中得到负值

    c++ - 如何在冒泡排序中添加一个计数器来统计数字比较?

    javascript - 如何解决计数器的追加值?

    c++ - 使用constexpr返回函数值C++的模板函数调用

    c++ - 球体 line_interpolate - 但在另一个方向

    c++ - 在基于 Visual Studio MFC 的应用程序中禁用事件处理程序

    c++ - 如何在其值可用于常量表达式的类中初始化数组?

    java - 使用 undefined symbol 加载 JNI 时崩溃

    c++ - constexpr 函数可以包含标签吗?