考虑以下代码:
class SmallObj {
public:
int i_;
double j_;
SmallObj(int i, double j) : i_(i), j_(j) {}
};
class A {
SmallObj so_;
int x_;
public:
A(SmallObj so, int x) : so_(so), x_(x) {}
int something();
int sox() const { return so_.i_; }
};
class B {
SmallObj* so_;
int x_;
public:
B(SmallObj* so, int x) : so_(so), x_(x) {}
~B() { delete so_; }
int something();
int sox() const { return so_->i_; }
};
int a1() {
A mya(SmallObj(1, 42.), -1.);
mya.something();
return mya.sox();
}
int a2() {
SmallObj so(1, 42.);
A mya(so, -1.);
mya.something();
return mya.sox();
}
int b() {
SmallObj* so = new SmallObj(1, 42.);
B myb(so, -1.);
myb.something();
return myb.sox();
}
方法“A”的缺点:
- 我们的具体使用
SmallObject
使我们依赖于它的定义:我们不能只是向前声明它,
- 我们的实例对我们的实例
SmallObject
来说是唯一的(不共享),
接近“B”的缺点有几个:
- 我们需要建立所有权合同并让用户知道它,
- 必须在
B
创建之前执行动态内存分配,
- 需要间接访问这个重要对象的成员,
- 如果我们要支持您的默认构造函数案例,我们必须测试空指针,
- 破坏需要进一步的动态内存调用,
反对使用自动对象的论据之一是按值传递它们的成本。
这是可疑的:在许多琐碎的自动对象的情况下,编译器可以针对这种情况进行优化并在线初始化子对象。如果构造函数是微不足道的,它甚至可以在一个堆栈初始化中完成所有事情。
这是 GCC 对 a1() 的 -O3 实现
_Z2a1v:
.LFB11:
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
subq $40, %rsp ; <<
.cfi_def_cfa_offset 48
movabsq $4631107791820423168, %rsi ; <<
movq %rsp, %rdi ; <<
movq %rsi, 8(%rsp) ; <<
movl $1, (%rsp) ; <<
movl $-1, 16(%rsp) ; <<
call _ZN1A9somethingEv
movl (%rsp), %eax
addq $40, %rsp
.cfi_def_cfa_offset 8
ret
.cfi_endproc
突出显示的 ( ; <<
) 行是编译器在一次就地构建 A 和它的 SmallObj 子对象。
a2() 的优化非常相似:
_Z2a2v:
.LFB12:
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
subq $40, %rsp
.cfi_def_cfa_offset 48
movabsq $4631107791820423168, %rcx
movq %rsp, %rdi
movq %rcx, 8(%rsp)
movl $1, (%rsp)
movl $-1, 16(%rsp)
call _ZN1A9somethingEv
movl (%rsp), %eax
addq $40, %rsp
.cfi_def_cfa_offset 8
ret
.cfi_endproc
还有 b():
_Z1bv:
.LFB16:
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
.cfi_lsda 0x3,.LLSDA16
pushq %rbx
.cfi_def_cfa_offset 16
.cfi_offset 3, -16
movl $16, %edi
subq $16, %rsp
.cfi_def_cfa_offset 32
.LEHB0:
call _Znwm
.LEHE0:
movabsq $4631107791820423168, %rdx
movl $1, (%rax)
movq %rsp, %rdi
movq %rdx, 8(%rax)
movq %rax, (%rsp)
movl $-1, 8(%rsp)
.LEHB1:
call _ZN1B9somethingEv
.LEHE1:
movq (%rsp), %rdi
movl (%rdi), %ebx
call _ZdlPv
addq $16, %rsp
.cfi_remember_state
.cfi_def_cfa_offset 16
movl %ebx, %eax
popq %rbx
.cfi_def_cfa_offset 8
ret
.L6:
.cfi_restore_state
.L3:
movq (%rsp), %rdi
movq %rax, %rbx
call _ZdlPv
movq %rbx, %rdi
.LEHB2:
call _Unwind_Resume
.LEHE2:
.cfi_endproc
显然,在这种情况下,我们付出了沉重的代价来传递指针而不是值。
现在让我们考虑以下代码:
class A {
SmallObj* so_;
public:
A(SmallObj* so);
~A();
};
class B {
Database* db_;
public:
B(Database* db);
~B();
};
从上面的代码中,您对 A 的构造函数中“SmallObj”的所有权的期望是什么?您对 B 中“数据库”的所有权的期望是什么?您是否打算为您创建的每个 B 构建一个唯一的数据库连接?
为了进一步回答您偏爱原始指针的问题,我们只需要查看 2011 C++ 标准,该标准引入了std::unique_ptr
和std::shared_ptr
帮助解决自 Cs 以来存在的所有权歧义strdup()
(返回指向字符串副本的指针,记得释放)。
标准委员会提出了一个observer_ptr
在 C++17 中引入的提议,它是一个围绕原始指针的非拥有包装器。
将这些与您喜欢的方法一起使用会引入很多样板:
auto so = std::make_unique<SmallObject>(1, 42.);
A a(std::move(so), -1);
我们在这里知道我们分配a
的实例的所有权so
,因为我们通过明确授予它所有权std::move
。但是所有这些都是明确的成本字符。对比:
A a(SmallObject(1, 42.), -1);
或者
SmallObject so(1, 4.2);
A a(so, -1);
所以我认为总的来说,很少有情况支持小对象的原始指针进行组合。您应该查看您的材料以得出结论,因为您似乎在何时使用原始指针的建议中忽略或误解了因素。