Wednesday 18 January 2012

Circular references in constructors

It is not uncommon to have situation when class A references class B and vice versa. This is known as circular dependency and is resolved by using forward declarations.

It is valid and safe to use mutually referenced classes if we initialize references after fully creating classes' instances. But what happens if references are assigned (and probably used) in constructors? Constructor of first class initializes reference to an instance of the second class that is yet to be created...This sounds silly and we can expect all sorts of things - from compile-time and run-time errors to programs running perfectly fine (well, if we are lucky enough...).

References can be implemented as pointer or reference types.

Let's have a look at pointer-based implementation and let us assume we have structures (or classes) implemented like this:

S1.h:

S1.cpp:

S2.h:

S2.cpp:

main.cpp:

We had to pass 0 to S1 constructor as s2 had not yet been declared nor created. This makes S1 constructor to raise access violation exception when it tries to dereference m_pS2 in order to call S2 methods.

If we used references instead of pointers, we would not even have a chance to write the code that compiles:

S3.h:

S3.cpp:

S4.h:

S4.cpp:

main.cpp:

A simple fact that we cannot have a reference (or address) of the object that hasn't yet been created leads to conclusion that it is impossible to create instances of mutually dependent classes where references must be initialized in constructor.

But there is one case when this is actually possible. Possible but not safe. If these structures are members of another (container) structure, it is possible to initialize references in container's constructor initializer list.


main.cpp:

Output:
S1::S1()
S2::Foo()
S2::PrintVal(): -858993460
S2::S2()
S1::Foo()
S1::PrintVal(): 1
S5::PrintVals()
S2::PrintVal(): 2
S1::PrintVal(): 1
S3::S3()
S4::Foo()
S4::PrintVal(): -858993460
S4::S4()
S3::Foo()
S3::PrintVal(): 1
S6::PrintVals()
S4::PrintVal(): 2
S3::PrintVal(): 1

We can see here that:
  • It is possible to set circular references between two classes during their construction if they are members of some container class
  • It is not safe to use circular references in constructors of their classes: dereferencing (and using) reference of the object that is not fully constructed leads to undefined behaviour. In our examples, the wrong value of the member of the class that had not yet been constructed was printed (-858993460 instead of 2)
  • It is safe to use circular references once objects are fully created S5::PrintVals() and S6::PrintVals() print correct values of members of referenced objects. This is similar to the case when some container class A has a member B that must be initialized with the pointer/reference to A (this or *this): that is safe as long as B's constructor doesn't try to access A's members or call A's methods. B has to wait for A to be completely constructed in order to use A's reference safely
One additional note: it is always a good practice to decouple classes (break dependencies between them) as much as possible. This is usually done by using interfaces or some other patterns (e.g. Observer pattern).

No comments: