Featured image of post Circular Dependencies

Circular Dependencies

How to handle or eliminate circular dependencies

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