11

Suppose, I have class Boss and Worker; Boss has a Worker and Worker holds a reference of Boss:

Boss.h

#include "Worker.h"
class Boss{
public:
    Worker worker;
};

Worker.h

class Boss;
class Worker{
    Boss* boss;
};

In C++, Worker doesn't need Boss to compile, but I am not comfortable to see the word "Boss" appear in Worker and the word "Worker" appears in a "Boss" at the same time. Also, if this design moves to other language that doesn't have systems like .h and .cpp (e.g.:Java), it becomes dependent on each other. So, my problem is, if the C++ code has such pattern, even Worker doesn't require Boss to compile, is it still a flawed design need to be fixed? If so, how can I fix it?

  • Another approach is to "shoehorn" the two classes together into one. That way Worker and Boss can share their members and methods. This may or may not be appropriate in your situation. It really depends whether you are going to declare lots of instances of Worker or sub class it etc. – gornvix Nov 20 '19 at 17:51
  • Like you could put all the shared members and methods in a base class and derive Worker and Boss from that base class. – gornvix Nov 20 '19 at 17:57
23

There is nothing which is fundamentally flawed about this idea. What you have is two relationships. Boss owns one or more Workers. And Worker has a non-owning reference to a Boss. The use of a raw pointer suggests that Worker does not own the pointer it stores; it's merely using it. This means that it does not control that object's lifetime.

There is nothing wrong with such a relationship per-se. It all depends on how it gets used.

For example, if the Boss reference that Worker stores is the actual Boss object instance that owns the Worker, then everything is fine. Why? Because presumably, a Worker instance cannot exist without a Boss who owns it. And because the Boss owns it, this guarantees that the Worker will not outlive the Boss it references.

Well... kinda. And this is where you start getting into potential problems.

First, by the rules of C++ construction, Boss::worker is constructed before the Boss instance itself. Now, Boss's constructor can pass a this pointer to Worker's constructor. But Worker cannot use it, since the Boss object has not yet been constructed. Worker can store it, but it can't access anything in it.

Similarly, by the rules of C++ destruction, Boss::worker will be destroyed after the owning Boss instance. So Worker's destructor cannot safely use the Boss pointer, since it points to an object whose lifetime has ended.

These limitations can sometime lead to having to use two-stage construction. That is, calling Worker back after Boss has been fully constructed, so that it can communicate with it during construction. But even this may be OK, particularly if Worker doesn't need to talk to Boss in its constructor.

Copying also becomes a problem. Or more to the point, the assignment operators become problematic. If Worker::boss is intended to point back to the specific Boss instance that owns it, then you must never copy it. Indeed, if that's the case, you should declare Worker::boss as a constant pointer, so that the pointer gets set in the constructor and nowhere else.

The point of this example is that the idea you've defined is not, by itself, unreasonable. It has a well-defined meaning and you can use it for many things. You just have to think about what you're doing.

| improve this answer | |
4

I will try to refine a point raised in gnasher729's answer.

We should make a careful distinction between:

  • Ownership - Boss owns a Worker. If Boss is destroyed, the Worker it owns must be destroyed simultaneously.
  • Independent existence (entity)
    • A Boss and a Worker can be in existence with individual lifetime. If one is destroyed, the other is not necessarily destroyed.
    • That said, a Boss needs to be able to communicate with a specific instance of Worker; likewise, a Worker needs to be able to communicate back to a specific Boss.

It is unclear if the question belongs to either case.

If Boss and Worker can exist independently, then one only needs to implement a mutual communication mechanism between them, without having each keeping a reference to the other. It is possible to use non-owning pointers for this purpose, as long as one implements a mechanism to reset pointers in every surviving instance whenever an instance is deleted.

A code example that tries to solve this:

// this class lists everyone and acts as the communication hub
class CompanyDirectory
{
    // owning reference to boss and all workers
    std::unique_ptr<Boss> boss;
    std::unordered_map<int, unique_ptr<Worker>> workers;
public:
    CompanyDirectory() { ... } // either ctor, or use two-part initialization
    Boss& GetBoss() { return *boss; }
    Worker& GetWorker(int workerId) { return *workers.at(workerId); }
};

class Boss
{
    // non-owning pointer to the communication hub
    CompanyDirectory* directory; 
public:
    void Promote(int workerId, Position position)
    {
        directory->GetWorker(workerId).SetPosition(position);
    }
};

// likewise, each Worker has a non-owning pointer/reference 
// to the communication hub that is CompanyDirectory.

In this example, the fetching of the Boss upon request by a Worker, or fetching aWorkerby ID upon request byBossor by a fellow worker, is the responsibility of theCompanyDirectory` class.

If a Worker instance is invalidated, it is also the responsibility of CompanyDirectory, specifically its retrieval methods (GetBoss, GetWorker), to throw an exception.

| improve this answer | |
0

There is nothing wrong with this design.

Java doesn't care about what order to compile things in, it usually just magically works. Well, I've found a few horrendously complicated cases involving static initialization for public static variables and class-loading order where I ran into problems, but that's extremely rare.

If you have an issue, it will be with saving these objects. In your database (or database mapping) you have to allow at least one pointer to be null. In your case, you'd have to allow either a user with a null boss or a boss without any users (or both).

If you use an ORM, you'll be fine, but if you write your own way of serializing or persisting these objects, you may have to detect loops so you don't go into an infinite loop trying to save your object graph.

That said, people do this all the time. Rails has a "last_modifier" which references the User. When you edit a user, you become that User's "last modifier" so the table circular-references itself (self-referential). Whenever you store a tree in a SQL database, each row usually has a parent_id pointer that references another row in the same table.

| improve this answer | |
0

Out Of The Tar Pit (PDF), published in 2006 describes functional relational programming. Think about your classes as tables in a relational database. How would you construct this relationship?

Boss and Worker would be “Employees”. The relationship between the boss and the worker is a one to many relationship: each employee should have one boss. This relationship could be implemented as a column in the employee table identifying this as an employee attribute or it could be represented as a separate table which may add additional capabilities (multiple bosses?).

Not every characteristic of an object necessarily belongs in the object. External relationships can be valuable, flexible, and help to reduce redundancy (and bugs).

| improve this answer | |
-1

In some languages (Objective-C, Swift, I think C#, and C++ with the right pointer class, you have owning references which keep an object from being deallocated, and circular owning references are usually frowned upon because the keep objects from being deallocated.

In the case of Boss and Worker that's nonsense. If the person represented by the Boss object is fired or dies and the Boss object should be destroyed as a consequence, the Worker objects shouldn't just disappear because the workers are still there. In practice, if the boss leaves the company, then the software handling this will have to change the boss pointers in all the worker objects to something else. To a null pointer or to a different boss pointer.

Now it may be a much better design to have one place that manages the relationships between bosses and workers. So the worker object doesn't have a pointer to its boss, but asks who his boss is. That works much better in software than in real life. And is much more flexible in that a worker may have two or three bosses, or none.

| improve this answer | |
  • The example given is explicitly a non-owning reference and a value. This may be non-traditional for Boss and Worker, but would you have the same complaint for Tree and Leaf? – Caleth Jun 24 '16 at 11:39
  • The discussion wasn't about "Tree" and "Leaf". – gnasher729 Jun 24 '16 at 12:27
  • 1
    In garbage collected languages like C# and Java, even circular pointers can be freed. Circular references aren't "frowned upon". – Greg Burghardt Jun 24 '16 at 12:54
  • Circular references in some ways are easier to handle for deallocation on a memory managed language that uses a GC like C# does. When the GC runs if that object graph does not have a router reference it will be deleted it's entirety. The fact that there are references inside the object graph are irrelevant. – Eric Johnson Jun 24 '16 at 16:53

Not the answer you're looking for? Browse other questions tagged or ask your own question.