在不复制或取消引用的情况下编写包装器 class

Writing a wrapper class without copying or dereferencing

假设我想围绕另一个 class 实例创建一个包装器,但我不想移动或复制那个原始 class 实例。也许我要包装的实例是在堆上声明的,并且很多其他东西都指向它。

我可以这样做:

class SomeClass {
 public:
  void Bar(int);
};

class Wrapper {
 public:
  Wrapper(SomeClass *some_class) {
    data = some_class;
  }

  void Foo() {
    data->Bar(42);
  }

 private:
  SomeClass *data;
};

然而,这增加了一个间接级别,因为必须取消引用数据指针。与将复制 SomeClass:

的此实现相比
class Wrapper {
 public:
  Wrapper(SomeClass some_class) : data(some_class) {}

  void Foo() {
    data.Bar(42);
  }

 private:
  SomeClass data;
};

这避免了取消引用,但现在它不是包装器(加上副本的开销)。

有没有一种方法可以在不复制或移动包装对象的情况下避免取消引用来编写包装器?

我想你可以做一些事情,比如将 SomeClass 实例静态转换为 Wrapper 实例,因为两个 classes 的数据布局应该相同(因为 Wrapper 中没有 vtable 或额外数据class),但这永远不会通过代码审查。

Is there a way to write the wrapper in a way that avoids the dereference without copying or moving the wrapped object?

想不出。

编写包装器时,您有两种选择。您可以复制或存储引用(指针和引用,即 SomeClass&)。

我建议使用参考。从句法上讲,可以像复制一样使用引用。我不会担心在 运行 时间实际取消引用的成本(毕竟,引用仍然是封面下的指针),除非它在您的用例中变得令人望而却步。

class Wrapper {
  public:
    Wrapper(SomeClass& some_class) : data(some_class) {}

    void Foo() {
      data.Bar(42);
    }

  private:
    SomeClass& data;
};

通过这种方法,只要包装器引用的对象比包装器寿命长,包装器就可以很好地使用。如果包装器的寿命比主要对象长,您将 运行 陷入悬空引用问题。

你的问题的答案对于人类来说是否定的,对于编译器来说是的。

您不能编写一个包装器来执行您描述的操作。

但是,带上你的 Wrapper class 。如果编译器可以做出正确的假设,它可以自由地优化间接寻址。

例如:

inline void foo(Wrapper x) {
  x.Foo();
}

int main() {
  auto v = std::make_unique<SomeClass>();
  Wrapper tmp(v.get();
  foo(tmp);
}

我敢打赌你不会因为在这里取消引用包装而支付甜甜圈。

我不确定您为什么要如此避免取消引用,因为归根结底,内存访问就是内存访问。它在堆栈上(并且您使用 . 运算符访问它)或在堆上(-> 运算符)这一事实在很大程度上是无关紧要的。

为了说明这一点,这里有一段简单的代码:

class foo {
    public:
    int a;
};

int main()
{
    foo f1;
    f1.a = 0;

    return 0;
}

及其对应的程序集:

push    rbp
mov     rbp, rsp 
mov     DWORD PTR [rbp-16], 0 // f1.a = 0
mov     eax, 0 
pop     rbp
ret

比较一下:

class foo {
    public:
    int a;
};

int main()
{
    foo *f2 = new foo();
    f2->a = 0;

    return 0;
}

装配:

push    rbp
mov     rbp, rsp
sub     rsp, 16
mov     edi, 4
call    operator new(unsigned long)
mov     DWORD PTR [rax], 0
mov     QWORD PTR [rbp-8], rax
mov     rax, QWORD PTR [rbp-8] // Address of f2
mov     DWORD PTR [rax], 0     // f2->a = 0
mov     eax, 0
leave
ret

在一天结束时,你有一个额外的汇编指令(为了清楚起见,在这个未优化的例子中),这是因为在第一个版本中你可以从堆栈指针偏移并需要加载地址在第二个版本中。这在运行时绝对不会产生明显的影响,尤其是当您进行多重访问时,您将拥有一个地址负载,然后为以下所有访问获得偏移量。

不要微优化无关紧要的事情。

根据@Frank 的想法,如果您在堆栈上声明它并让它内联所有内容,编译器足够聪明,可以编译掉包装器 class。这个:

class SomeClass {
 public:
  void Bar(int bar) {
      data = bar;
  }
 private:
  int data;
};

int main() {
    SomeClass *instance = new SomeClass();
    instance->Bar(42);
}

生成与此包装器相同的程序集 class(根据 godbolt 与 gcc 6.3 和 -O3):

class SomeClass {
 public:
  void Bar(int bar) {
      data = bar;
  }
 private:
  int data;
};

class Wrapper {
 public:
  Wrapper(SomeClass *some_class) {
    data = some_class;
  }

  void Foo() {
    data->Bar(42);
  }

 private:
  SomeClass *data;
};

int main() {
    SomeClass *instance = new SomeClass();
    Wrapper wrapper(instance);
    wrapper.Foo();
}

但这会产生额外的汇编(甚至超出对 'new' 的调用):

class SomeClass {
 public:
  void Bar(int bar) {
      data = bar;
  }
 private:
  int data;
};

class Wrapper {
 public:
  Wrapper(SomeClass *some_class) {
    data = some_class;
  }

  void Foo() {
    data->Bar(42);
  }

 private:
  SomeClass *data;
};

int main() {
    SomeClass *instance = new SomeClass();
    Wrapper *wrapper = new Wrapper(instance);
    wrapper->Foo();
}

因此,只要包装器 class 以编译器可以优化它的方式在堆栈上构建,就没有性能开销。如果包装器 class 在堆上声明,则无法进行优化。