次のコードを検討してください。
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
everyを作成する前に、動的メモリ割り当てを実行する必要があります。
- この重要なオブジェクトのメンバーにアクセスするには、間接化が必要です。
- デフォルトのコンストラクターのケースをサポートする場合は、null ポインターをテストする必要があります。
- 破壊にはさらに動的メモリ呼び出しが必要です。
自動オブジェクトの使用に反対する理由の 1 つは、それらを値渡しするコストです。
これは疑わしいです: 自明な自動オブジェクトの多くの場合、コンパイラはこの状況に合わせて最適化し、サブオブジェクトをインラインで初期化できます。コンストラクターが単純な場合、1 回のスタック初期化ですべてを実行できる場合もあります。
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 に対して一意のデータベース接続を構築するつもりですか?
生のポインターを優先するというあなたの質問にさらに答えるには、Cs (文字列のコピーへのポインターを返す、解放することを忘れないでstd::unique_ptr
ください。 )。std::shared_ptr
strdup()
標準化委員会の前に、C++17 に を導入するという提案がありobserver_ptr
ます。これは、未加工のポインターの非所有ラッパーです。
これらを好みのアプローチで使用すると、多くのボイラープレートが導入されます。
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);
したがって、合成のために小さなオブジェクトの生のポインターを優先するケースは全体的にほとんどないと思います。生のポインターをいつ使用するかの推奨事項で見落としたり誤解したりする可能性があるため、結論に至るまでの資料を確認する必要があります。