【问题标题】:mutually dependent local classes (or mutually recursive lambdas)相互依赖的本地类(或相互递归的 lambda)
【发布时间】:2013-12-13 23:23:56
【问题描述】:

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

这个想法是有以下模式:

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.cpp 中声明和定义类,在 SomeClass::someMethod() 之前。我觉得这并不优雅,因为这些课程不仅不是 SomeClass::someMethod() 本地的,甚至不是 SomeClass 本地的。

  2. 在 SomeClass.h 中声明嵌套在 SomeClass 中的类,并在 SomeClass.cpp 中定义它们。我也不喜欢这个解决方案,因为不仅这些类不是 SomeClass::someMethod() 本地的,而且它确实污染了 SomeClass.h,除了语言的限制之外没有其他充分的理由。

因此有两个问题:是否有可能将类放在SomeClass::someMethod() 的本地?如果没有,您是否看到更优雅的解决方法?

【问题讨论】:

  • 为什么在方法中需要它?您只能在 .cpp 文件中声明/定义它们,并且不会污染 .h 文件
  • @BryanChen:我并不是真的需要它,只是如果可以的话它会更优雅:如果类是方法的实现细节,那么在方法内部定义它是它所属的地方。只是良好的 OOP 封装。
  • @Boris 它的封装,但它不是 OOP。
  • 好吧,这对我来说很有意义。但是,这意味着您将拥有一个大方法,并且您不能对结构 AB 进行单元测试
  • 仍然,为什么需要两个相互依赖的结构?如果你展示了这个的实际用法,也许会更容易回答。

标签: c++ class struct forward-declaration


【解决方案1】:

实现一个虚拟的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() ; } }
} ;

【讨论】:

  • 啊,好吧,那我们得把恐怖程度调高一点。
【解决方案2】:

由于答案似乎是:“不可能有干净的相互依赖的本地类”,事实证明我最喜欢的解决方法是将逻辑移到结构本身之外。正如 remyabel 在问题的 cmets 中所建议的那样,可以通过创建第三个类来完成,但我最喜欢的方法是创建相互递归的 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。

【讨论】:

    【解决方案3】:

    我过去也认为这也是不可能的,但有人认为 Y 组合器可以很好地利用,现在是 2021 年,而 constexpr 有助于在可能比 Haskell 更好的语言中创建一种语言。此外,它是编写函数式语言编译器时经常出现的一类问题的基础......

    要找到最佳解决方案,需要横向思考如何破解这种先有鸡还是先有蛋的局面:

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

    然后我们基本上使用 Y 组合器的第一次迭代。在 A::foo 的上下文中,我们使用类扩展器 crB 创建 B 对象的精确类型,并将 this 转换为该类型的指针以“调用”它。

    这就是有趣的地方。如果代码的类型足够好,编译器可以推断出我们打算进行相互尾递归,并省略对 jmp 的调用,该过程对于正确定位的功能代码至关重要。

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

      #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
    

    所有尾递归调用都已被省略。翻译基本完美。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-03-25
      • 1970-01-01
      • 1970-01-01
      • 2013-09-03
      • 2011-04-26
      • 2017-05-09
      相关资源
      最近更新 更多