Consider the following code:
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();
}
The disadvantages with approach 'A':
- our concrete use of
SmallObject
makes us dependent on its definition: we can't just forward declare it,
- our instance of
SmallObject
is unique to our instance (not shared),
The disadvantages to approach 'B' are several:
- we need to establish an ownership contract and make the user aware of it,
- a dynamic memory allocation must be performed before every
B
is created,
- indirection is required to access the members of this vital object,
- we must test for null pointers if we are to support your default constructor case,
- destruction requires a further dynamic memory call,
One of the arguments against using automatic objects is the cost of passing them by value.
This is dubious: in many cases of trivial automatic objects the compiler can optimize for this situation and initialize the sub-object in-line. If the constructor is trivial, it may even be able to do everything in one stack initialization.
Here is GCC's -O3 implementation of a1()
_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
The highlighted (; <<
) lines are the compiler doing the in-place construction of A and it's SmallObj sub-object in a single shot.
And a2() optimizes very similarly:
_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
And there there's 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
Clearly, in this case, we paid a heavy price to pass by pointer instead of value.
Now lets consider the following piece of code:
class A {
SmallObj* so_;
public:
A(SmallObj* so);
~A();
};
class B {
Database* db_;
public:
B(Database* db);
~B();
};
From the above code, what is your expectation of ownership of "SmallObj" in the constructor of A? And what is your expectation of ownership of "Database" in B? Do you intend to construct a unique database connection for every B you create?
To further answer your question of favoring raw pointers, we need look no further than the 2011 C++ standard which introduced the concepts of std::unique_ptr
and std::shared_ptr
to help resolve ownership ambiguity that has existed since Cs strdup()
(returns a pointer to a copy of a string, remember to free).
There is a proposal before the standards committee to introduce an observer_ptr
in C++17, which is a non-owning wrapper around a raw pointer.
Using these with your preferred approach introduces a lot of boiler plate:
auto so = std::make_unique<SmallObject>(1, 42.);
A a(std::move(so), -1);
We know here the a
has ownership of the so
instance we allocated, as we explicitly grant it ownership via std::move
. But all that being explicit costs characters. Contrast with:
A a(SmallObject(1, 42.), -1);
or
SmallObject so(1, 4.2);
A a(so, -1);
So I think overall there is very little case for favoring raw pointers for small objects for composition. You should review your material leading you to the conclusion as it seems likely you have overlooked or misunderstood factors in a recommendation of when to use raw pointers.