SH: I finally found some time to read your article series in Overload 52&53, and I've got some comments:
PB: Thanks for spending the time to provide some feedback. It's very much appreciated.
SH: I used to work on a framework that supported connections between logic-gate-like objects in much the same way as you describe. It was to be used in control systems in industrial user interfaces (real physical knobs and buttons). Thus I think I've got some experience to draw from.
PB: Yes, that's exactly what we have. In our case it's mainly buttons and lamps - very few knobs.
SH: You wrestle with the problem of copying events (or the objects that contain them). Funny enough that you say yourself that you often try to solve the wrong problem if you find that the solution seems elusive. I couldn't agree more. I think events shouldn't have value semantics. Here's why:
You rightly associate an event with an output in a logic gate. The logic gate would be represented by an object that contains the event as a member object. What does it mean then to copy such a gate with its event member? It would be much like copying a gate on a schematic drawing, right? If you ever worked in hardware design, you know that copying a gate gives you a gate of the same type but with its pins detached. Copying a gate doesn't copy its connections!
PB: Absolutely. I haven't done any electronics at all, but I have worked alongside electrical and electronics engineers. And, I agree that copying a logic gate wouldn't make sense if its connections were copied as well.
However, I'm interested in the general problem of de-coupling objects in one software layer from the higher-level objects that observe them. Perhaps there are some objects for which a copy operation really should copy the connections. I couldn't think of any specific examples, but I suspected that different copy semantics would be appropriate for different types of object. In particular, some objects shouldn't be copyable at all.
SH: What it really boils down to is that a gate has a unique identity (which is usually shown in schematics, i.e. "U205"). You can't have two objects with the same identity. Copying a gate creates a new gate with a new identity. Hence gates can not have value semantics. The result is that storing them in STL containers directly is a bad idea. You store pointers to them instead.
PB: Now you're losing me. I'm familiar with the idea of two broad categories of objects: 1) those with value semantics and 2) those with reference semantics. I appreciate, too, that identity is often of little or no importance for objects in category 1, whereas it is usually crucial for category 2 objects. However, I don't see why an object with identity shouldn't have value semantics. And I would be quite upset if objects with identity could/should not be stored in STL containers.
For example, what's wrong with a std::vector<Customer>? Real customers certainly have identity and I'd expect objects of the Customer class to have identity, too. You might argue that it doesn't make sense to copy a Customer (which is true), but it makes perfect sense to store Customers in a vector, and that requires Customer objects to be copyable. My Events are just like Customers: it makes no sense to copy them, but perfect sense to store them in STL containers.
SH2: I do see a conflict between object identity and value semantics. Let me quote from Josuttis' Standard Library Book (page 135): "All containers create internal copies of their elements and return copies of those elements. This means that container elements are equal but not identical to the objects you put into the container."
I think that this makes it quite clear that a std::vector<Customer> might not be such a good idea. As a further hint, consider what would happen if you'd like to hold a customer in two containers at the same time (say, one that holds all customers, and another that holds customers who are in debt).
What I'm saying is that if the only reason to make a class copyable is to be able to put them into STL containers, you're probably trying to do the wrong thing.
SH: The pointers may well be smart pointers. The details depend on your ownership model. It is not always necessary to use reference counted pointers. If you have a different method to ensure that connections are removed before the gates are deleted, then a bare pointer may be adequate.
PB: Agreed.
SH: Note that you don't necessarily need to prevent event copying altogether. It may well be useful to be able to copy events, but the key is that copying an event does not copy the connections. I feel, however, that this style of copying is better implemented through a separate clone function instead of the copy constructor.
PB: This really depends on whether we choose value or reference semantics, I think. No, on second thoughts, it depends on whether the objects in question are polymorphic (the virtual function kind of polymorphism). The value/reference semantics design decision is separate.
SH2: In the polymorphic case you have no choice but storing pointers in the container anyway. Copy construction is not needed here. If you nevertheless want to make copies of your objects, they need a virtual clone member function. The value/reference decision is not entirely separate, since you can not have value semantics with polymorphism in the same object. If you wanted to model that, you'd end up with a handle-body pair of objects, which is just another variant of the smart pointer theme.
SH: Regarding event ownership I think that a hierarchical ownership model may well be better than a distributed "shared ownership" model. If we carry on a little longer with the hardware analogy, I would propose to have a "Schematic" object that owns all the gates in it. Deleting the schematic object deletes all connections and gates. The problem then is reduced to "external" connections that go between the schematic and the outside world. Internal connections don't need to be implemented with smart pointers.
PB: Again, I agree that a hierarchical ownership model has benefits. In fact, I suspect our software could be improved by modeling the hardware more closely - tools contain modules, which contain other modules, which contain I/O boards - and the logic gates would be owned by the module that contains them. In practice, though, this is not particularly easy to do and I felt that discussing the short-comings of the existing software would distract from the main point of the article. The difficulty boils down to the difference you highlight, here. External connections and internal connections are not distinguished, so we would have to use a mechanism that supports the more general ownership model for both.
SH2: It would certainly go too far for the purpose of the article to introduce several levels of hierarchy. The point I was hinting at however was that looking at the problem from one level up in the hierarchy might render a different - and maybe more adequate - design. The problem here is a general problem with the observer pattern: Who owns/controls the connections? It is by no means clear that it should be the subject that owns them. This would be analogous to a chip on a PCB that owns the wires connected to its outputs. Wouldn't it be more natural to think that all wires belong to the PCB itself? Each output could still have a container of pointers to connections for managing the updates, but it wouldn't necessarily own them from a lifetime management perspective.
Now, as you rightly point out, the hardware analogy isn't necessarily always the right one. So it will likely depend on the situation what kind of ownership model you would choose. This makes me wonder whether it would be sensible and feasible for a general purpose observer implementation to provide some latitude in this respect, maybe through policies (in Alexandrescu's sense).