This is somewhat of a broad question that seems to have no one true answer.
I've been confused about the initialization of composed objects for quite some time. I've been formally taught to supply getters and setters for all member data and to favor raw pointers to objects instead of automatic objects - this seems to contrast with what many people on Stack Overflow (such as this popular post) advise.
How, then, should I go about initializing object-composed objects?
This is the way I would attempt initialization using what I've learned in school:
class SmallObject1 {
public:
SmallObject1() {};
};
class SmallObject2 {
public:
SmallObject2() {};
};
class BigObject {
private:
SmallObject1 *obj1;
SmallObject2 *obj2;
int field1;
int field2;
public:
BigObject() {}
BigObject(SmallObject1* obj1, SmallObject2* obj2, int field1, int field2) {
// Assign values as you would expect
}
~BigObject() {
delete obj1;
delete obj2;
}
// Apply getters and setters for ALL members here
};
int main() {
// Create data for BigObject object
SmallObject1 *obj1 = new SmallObject1();
SmallObject2 *obj2 = new SmallObject2();
int field1 = 1;
int field2 = 2;
// Using setters
BigObject *bobj1 = new BigObject();
// Set obj1, obj2, field1, field2 using setters
// Using overloaded contructor
BigObject *bobj2 = new BigObject(obj1, obj2, field1, field2);
return 0;
}
This design is appealing because it's readable (to me). The fact that BigObject
has pointers to its member objects makes it possible to initialize obj1
and obj2
after initialization. However, the dynamic memory could make the program more complicated and confusing down the road, thus ripe for memory leaks. Additionally, the use of getters and setters clutter up the class and may also make the member data too easy to access and mutate.
Is this actually bad practice? I often find times where I need to initialize a member object separately from its owner, which makes automatic objects unappealing. Additionally, I have considered letting larger objects construct their own member objects. This seems to make more sense from a security standpoint, but less sense from an object responsibility standpoint.
I've been formally taught to supply getters and setters for all member data and to favor raw pointers to objects instead of automatic objects
Unfortunately you were taught wrong.
There's absolutely no reason to favor raw pointers over any standard library constructs like std::vector<>
, std::array<>
, or if you need to std::unique_ptr<>
, std::shared_ptr<>
.
The most common culprit in buggy software is that (rolled your own) memory management exposes flaws, and even worse these are usually hard to debug.
I've been formally taught to supply getters and setters for all member data and to favor raw pointers to objects instead of automatic objects
Personally, I have no problem with having setters and getters for all data members. It is a good practice to have and can save a lot of grief especially if you venture into threads. In fact, many UML tools autogenerate them for you. You just need to be aware of what to return. In this particular example, don't return a raw pointer to SmallObject1 *
. Return SmallObject1 * const
instead.
The second part about
raw pointers
is done for educational purposes.
For your main question: the way you structure object storage depends on the larger design. Is BigObject
the only class that will ever use SmallObject
's? Then I would put them completely inside of the BigObject
as private members and do all the memory management there. If SmallObject
's are shared between different objects, and not necessarily of BigObject
class, then I would do what you did. However, I would store the references or pointers to const to them and not delete them in the BigObject
class's destructor - BigObject
didn't allocate them, it thus shouldn't be deleting.
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':
SmallObject
makes us dependent on its definition: we can't just forward declare it, SmallObject
is unique to our instance (not shared), The disadvantages to approach 'B' are several:
B
is created, 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.
Others have described optimization reasons, I'm now viewing it from a type / functional perspective. According to Stroustrup, 'It is the job of every constructor to establish the class invariant'. What is your class invariant here? It's important to know (and define!), otherwise you'll pollute your member functions with if
s to check whether the operation is valid - this is not much better than having no types at all. In the 90s we had classes like that, but nowadays we really hold on to the invariant definitions and want objects to be in a valid state all the time. (Functional programming goes a step further and tries to extract variable state from objects, so objects can be const.)
std::optional<SmallObject>
members. Optional usually allocates locally (vs. heap), thus you might benefit from cache locality. Note that many of us who prefer functional style consider builder an anti-pattern and only use it for deserialization, if at all. The reason behind, it's very difficult to reason about a builer (what comes out, will it succeed, which constructor gets calles). If you have your two ints, that's just that: two ints. Your best bet is usually just to keep them in separate variables, then it's up to the compiler to do all kinds of optimization. I wouldn't be surprised if the pieces would miraculously just fall in piece and your ints would be constructed 'in place', therefore no copy is required later.
OTOH, if you find that the same parameters 'get bounded' (get their value) in many places beforehand others, then you might introduce a type for them. In this case, your two ints will be a type (preferably a struct). You might decide if you want to make it a base class of BigObject
, a member, or just a separate class (you'll have to choose the third if you have multiple binding orders) - in either case, your constructor will now take the new class instead of the two ints. You might even consider deprecating your other constructor (the one taking two ints) as 1. the new object can be constructed easily, 2. it might be shared (eg when creating items in a loop). If you want to keep the old constructor, make one of them a delegate to the other.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.