假设我想围绕另一个类实例创建一个包装器,但我不想移动或复制该原始类实例。也许我想要包装的实例在堆上声明,许多其他东西指向它。
我可以这样做:
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实例的东西,因为这两个类的数据布局应该是相同的(因为Wrapper类中没有vtable或额外的数据),但是这样做永远不会通过代码审查。
答案 0 :(得分:2)
有没有办法以避免解除引用而不复制或移动包装对象的方式编写包装器?
我想不出任何一个。
当你编写包装器时,你有两个选择。您可以复制或存储引用(指针和引用,即SomeClass&
)。
我建议使用参考。从语法上讲,可以像使用副本一样使用引用。我不担心在运行时实际解除引用(毕竟,引用仍然是一个指针)的成本,除非它在您的用例中变得过高。
class Wrapper {
public:
Wrapper(SomeClass& some_class) : data(some_class) {}
void Foo() {
data.Bar(42);
}
private:
SomeClass& data;
};
使用这种方法,只要它引用的对象超过包装器,包装器就会很好用。如果包装器超过主对象,则会遇到悬空引用问题。
答案 1 :(得分:2)
我不确定你为什么要避免解除引用这么多,因为在一天结束时,内存访问是一种内存访问。它在堆栈上(并且您使用.
运算符访问它)或堆(->
运算符)这一事实在很大程度上是无关紧要的。
为了说明问题,这里有一段简单的代码:
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
在一天结束时,你有一个额外的汇编指令(为了清晰起见,在这个未经优化的例子中),这是因为在第一个版本中你可以从堆栈指针偏移并需要加载第二个版本中的地址。在运行时,这一点绝对没有明显的效果,特别是当您进行多次访问时,您将拥有一个地址加载,然后对以下所有访问进行偏移。
不要微观优化不重要的事情。
答案 2 :(得分:1)
你的问题的答案对于人而言是肯定的,对于编译者来说是肯定的。
你不能写一个做你所描述的包装器。
但是,请参加Wrapper
课程。如果可以做出正确的假设,那么Ther编译器可以自由地优化该间接。
例如:
inline void foo(Wrapper x) {
x.Foo();
}
int main() {
auto v = std::make_unique<SomeClass>();
Wrapper tmp(v.get();
foo(tmp);
}
我会把美元押在甜甜圈上,你不会因为在这里取消引用包装而支付费用。
答案 3 :(得分:0)
基于@Frank的一个想法,如果你在堆栈上声明它并让它内联所有内容,编译器就足够聪明地编译掉包装类。这样:
class SomeClass {
public:
void Bar(int bar) {
data = bar;
}
private:
int data;
};
int main() {
SomeClass *instance = new SomeClass();
instance->Bar(42);
}
生成与此包装类相同的程序集(根据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();
}
但是这会产生额外的装配(甚至超出了对&#39; new&#39;的召唤):
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();
}
因此,只要在堆栈上构造包装类,编译器就可以对其进行优化,那么就没有性能开销。如果在堆上声明包装类,则无法进行优化。