相互依赖的局部类(或相互递归的lambdas)

mutually dependent local classes (or mutually recursive lambdas)

本文关键字:递归 lambdas 依赖 局部      更新时间:2023-10-16

我经常在方法内部创建本地助手类,只要这样的类在本地有用,但在方法外部无关。我刚刚遇到一个案例,我想要两个相互依赖的本地类。

其想法是具有以下模式:

void SomeClass::someMethod()
{
    struct A
    {
        B * b;
        void foo() { if(b) b->bar(); };
    };
    struct B
    {
        A * a;
        void bar() { if(a) a->foo(); }
    };
}

但是由于A需要B,所以它没有编译。正向声明B有助于使行B * b;编译,但方法A::foo()仍然需要B的完整声明,编译器会抱怨。

我看到了两个解决方案:

  1. SomeClass::someMethod()之前的SomeClass.cpp中声明和定义类。我觉得这并不优雅,因为类不仅不是SomeClass::someMethod()的本地类,甚至不是SomeClass的本地类。

  2. 声明SomeClass.h中的类嵌套在SomeClass中,并在SomeClass.cpp中定义它们。我也不喜欢这个解决方案,因为不仅这些类不是SomeClass::someMethod()的本地类,而且它确实污染了SomeClass.sh,原因无非是语言的限制。

因此有两个问题:SomeClass::someMethod()本地的类是否可能?如果没有,你会看到更优雅的解决方案吗?

实现一个虚拟的a,供B使用,然后是真实的a。

struct virtA
{
  virtual void foo() = 0 ;
} ;
struct B
{
  virtA * a ;
  void bar() { if ( a) { a->foo() ; } }
} ;
struct A : public virtA
{
  B * b ;
  void bar() { if ( b) { b-> bar() ; } }
} ;

因为答案似乎是:"不可能有干净的相互依赖的本地类",所以我最喜欢的解决方法是将逻辑移到结构本身之外。正如雷米亚贝尔在问题评论中所建议的那样,这可以通过创建第三个类来完成,但我最喜欢的方法是创建相互递归的lambda函数,因为它可以捕获变量(因此在实际使用中使我的生活更轻松)。所以它看起来像:

#include <functional>
#include <iostream>
int main()
{
    struct B;
    struct A { B * b; };
    struct B { A * a; };
    std::function< void(A *) > foo;
    std::function< void(B *) > bar;
    foo = [&] (A * a) 
    {
        std::cout << "calling foo" << std::endl;
        if(a->b) { bar(a->b); }
    };
    bar = [&] (B * b)
    {
        std::cout << "calling bar" << std::endl;
        if(b->a) { foo(b->a); }
    };
    A a = {0};
    B b = {&a};
    foo(&a);
    bar(&b);
    return 0;
}

编译和打印:

calling foo
calling bar
calling foo

注意,必须手动指定lambda的类型,因为类型推理不能很好地使用递归lambda。

我过去也认为这是不可能的,但有一种挥之不去的想法,认为Y组合子可以得到很好的利用,现在是2021年了,constexpr有助于在一种可能比Haskell更好的语言中创建一种语言。此外,它是编写函数式语言编译器时经常发生的一类问题的基础。。。

为了找到最佳解决方案,需要对如何解决这种鸡/蛋的情况进行一些横向思考:

首先,我们不能在本地类中使用auto或模板,那么我们可以做些什么吗?我们可以定义作为函数的lambda——它们被发送的类型参数,忽略任何实例数据,允许它们是constexpr。这意味着我们可以定义一个函数来创建对象B,给定a的类型。。。但我们还没有完成,我们还有一个数据段需要处理。类创建函数需要知道这一点,否则我们必须显式地使用a::i从a段获取数据。因此,就像在汇编代码中一样,我们必须将数据和代码段分开,由于数据的类型更简单,我们将其放在依赖项列表的第一位,并将其作为虚拟基类派生两次,这是我为虚拟基类找到的为数不多(只有?)的有效用途之一(这种模式最好避免,但在这里,不可避免?)。

然后我们基本上处理Y组合子的第一次迭代。在A::foo的上下文中,我们使用类扩展程序crB来创建B对象的精确类型,并将其强制转换为该类型的指针";呼叫";

这就是它变得有趣的地方。如果代码的类型足够好,编译器可以推断出我们打算进行互尾递归,并取消对jmp的调用,jmp是一个对目标正确的函数代码至关重要的过程。

使用std::函数,它是使用编译器不友好的虚拟函数调用实现的,会破坏这种优化的任何机会,所以这种方法可能更友好,因为编译器可以访问所有涉及的类型,而不需要任何间接操作?

  #include <iostream>
  int main(int argc, char* argv[])
  {      
     struct Data
     {
        int i = 563;
     };
     constexpr auto crB = [](auto par)
     {
        using T = decltype(par);
        struct B :  public T, virtual public Data
        {
           void foo() {
              std::cerr << "B: " << i << "n";
              i = (3 * i) + 1;
              T::foo();
           }
        };
        return B();
     };
     struct A : virtual public Data
     {
        void foo()
        {
           std::cerr << "A: " << i << "n";
           i >>= 1;
           if (i == 1)
              return;
           using ForwardT = decltype(crB(A()));
           (i&1)? static_cast<ForwardT *>(this)->foo() : foo();
        };
     };
     auto binst = crB(A());
     binst.foo();
     return 0;
  }

那么,这种高级汇编语言的编译目的是什么呢?

  .LC0:
        .string "B: "
  .LC1:
        .string "n"
  .LC2:
        .string "A: "
  main::{lambda(auto:1)#1}::operator()<main::A>(main::A) const::B::foo():
        push    rbx
        mov     rbx, rdi
  .L3:
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:_ZSt4cerr
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        mov     rdi, rax
        mov     rax, QWORD PTR [rbx]
        mov     rax, QWORD PTR [rax-24]
        mov     esi, DWORD PTR [rbx+rax]
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
        mov     esi, OFFSET FLAT:.LC1
        mov     rdi, rax
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        mov     rax, QWORD PTR [rbx]
        mov     rdx, QWORD PTR [rax-24]
        add     rdx, rbx
        imul    eax, DWORD PTR [rdx], 3
        inc     eax
        mov     DWORD PTR [rdx], eax
  .L4:
        mov     esi, OFFSET FLAT:.LC2
        mov     edi, OFFSET FLAT:_ZSt4cerr
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        mov     rdi, rax
        mov     rax, QWORD PTR [rbx]
        mov     rax, QWORD PTR [rax-24]
        mov     esi, DWORD PTR [rbx+rax]
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
        mov     esi, OFFSET FLAT:.LC1
        mov     rdi, rax
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        mov     rax, QWORD PTR [rbx]
        mov     rdx, QWORD PTR [rax-24]
        add     rdx, rbx
        mov     eax, DWORD PTR [rdx]
        sar     eax
        mov     DWORD PTR [rdx], eax
        cmp     eax, 1
        je      .L1
        test    al, 1
        je      .L4
        jmp     .L3
  .L1:
        pop     rbx
        ret
  main:
        sub     rsp, 24
        mov     rdi, rsp
        mov     QWORD PTR [rsp], OFFSET FLAT:vtable for main::{lambda(auto:1)#1}::operator()<main::A>(main::A) const::B+24
        mov     QWORD PTR [rsp+8], 563
        call    main::{lambda(auto:1)#1}::operator()<main::A>(main::A) const::B::foo()
        xor     eax, eax
        add     rsp, 24
        ret
  _GLOBAL__sub_I_main:
        push    rax
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        pop     rcx
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        jmp     __cxa_atexit
  typeinfo for main::Data:
        .quad   vtable for __cxxabiv1::__class_type_info+16
        .quad   typeinfo name for main::Data
  typeinfo name for main::Data:
        .string "*Z4mainE4Data"
  typeinfo for main::A:
        .quad   vtable for __cxxabiv1::__vmi_class_type_info+16
        .quad   typeinfo name for main::A
        .long   0
        .long   1
        .quad   typeinfo for main::Data
        .quad   -6141
  typeinfo name for main::A:
        .string "*Z4mainE1A"
  typeinfo for main::{lambda(auto:1)#1}::operator()<main::A>(main::A) const::B:
        .quad   vtable for __cxxabiv1::__vmi_class_type_info+16
        .quad   typeinfo name for main::{lambda(auto:1)#1}::operator()<main::A>(main::A) const::B
        .long   2
        .long   2
        .quad   typeinfo for main::A
        .quad   2
        .quad   typeinfo for main::Data
        .quad   -6141
  typeinfo name for main::{lambda(auto:1)#1}::operator()<main::A>(main::A) const::B:
        .string "*ZZ4mainENKUlT_E_clIZ4mainE1AEEDaS_E1B"
  vtable for main::{lambda(auto:1)#1}::operator()<main::A>(main::A) const::B:
        .quad   8
        .quad   0
        .quad   typeinfo for main::{lambda(auto:1)#1}::operator()<main::A>(main::A) const::B
    .quad   0
    .quad   typeinfo for main::{lambda(auto:1)#1}::operator()<main::A>(main::A) const::B

所有的尾部递归调用都被取消了。翻译基本上是完美的。