How To Handle Circular Dependencies
Circular dependencies can raise challenges and frustration.
Eliminate Circular Dependencies
Any time A depends on B and B depends on A, a third
object C can be created where C depends on both
A and B. This eliminates the circular dependency entirely.
The following example has a problem.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
struct Vector2 {
int x = 0; int y = 0;
Vector2& operator=(const Vector3& p) {
// assigning 3d to 2d truncates
x = p.x;
y = p.y;
return *this;
}
};
struct Vector3 {
int x = 0; int y = 0; int z = 1;
Vector3& operator=(const Vector2& p) {
// assigning 2d to 3d sets the homogenous coor
x = p.x;
y = p.y;
z = 1;
return *this;
}
};
int main() {
Vector2 latLngA{3, 4};
Vector3 worldPosA{5, 5, 2};
// assigning 3d to 2d
latLngA = worldPosA;
Vector2 latLngB{6, 6};
Vector3 worldPosB{7, 8, 9};
// assigning 2d to 3d
worldPosB = latLngB;
}
|
This begets a compile error because Vector2 needs to know
about Vector3 but Vector3 has not been defined yet.
If we reverse the order, then we just get the reverse problem
where Vector3 would need to know about Vector2 but Vector2
won’t be defined yet.
The solution is to create a third higher level layer
that accepts and operates on our two vector types so
that our vector types don’t need to know about each other.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
struct Vector2 {
int x = 0; int y = 0;
};
struct Vector3 {
int x = 0; int y = 0; int z = 1;
};
Vector2& operator=(Vector2& p, const Vector3& q) {
p.x = q.x;
p.x = q.y;
return p;
}
Vector3& operator=(Vector3& p, const Vector2& q) {
p.x = q.x;
p.y = q.y;
p.z = 1;
return p;
}
|
Forward Declaration
Eliminating circular dependencies entirely is most desireable.
Another option is to forward declare.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
struct Vector3;
struct Vector2 {
int x = 0; int y = 0;
Vector2& operator=(const Vector3& p) {
x = p.x;
y = p.y;
return *this;
}
};
struct Vector3 {
int x = 0; int y = 0; int z = 1;
Vector3& operator=(const Vector2& p) {
x = p.x;
y = p.y;
z = 1;
return *this;
}
};
|
Here we simply tell Vector2
that Vector3
will exist
at some point in the future.
This allows the code to compile, however, Vector2
can
only refer to either references or pointers to Vector3
because we don’t yet know what the in memory layout
of Vector3
looks like.
Cover Photo
Credit: Joshua Lawrence