For C++ I find it useful to relax the whole immutability mindset. I don't mean to upset the purists out there but it's like if we're starting to fight the language and jump through all kinds of hoops to make everything we can immutable, including objects only used as local temporaries in a small number of functions, it might not be the wisest use of our time.
Still this is coming from one who greatly appreciates immutable designs, when practical to apply, and particularly pure functions free of external side effects. I've found it among the most useful practices to help reason about the system and thread safety and find more opportunities to parallelize code (not only with the goal of making things go faster, but making it so users don't have to wait for them to finish).
I also think it's helpful to zoom out of the bottom-up nature of designing objects trying to balance out every single possible use case in a vacuum and focus on the overall architectural design a bit, and see if you can solve things there instead, unless you're trying to write something so generalized in purpose that you're trying to contribute to boost or just write a very general-purpose library for world-wide consumption.
Enforcing Design Restrictions at the Architectural Level
To make that above statement a bit concrete since it's a bit fuzzy, for example the former architecture I was working with unified a lot of things under the
Command pattern. There was like a million lines of code just dedicated to implementing commands which model the operations users can perform, including some code written by third parties. And previously we just handed commands a pointer to our
Scene, which is like the bulk of our application state, for them to mutate at will. Which was gross as far as multithreading since we'd have to lock our threads and the whole application could come to a halt for hefty commands. Even the ones kind enough to utilize the progress bar would make the progress bar borderline unresponsive since we were executing the commands in the same thread as the UI thread. On top of that some commands, when they ran into exceptions, would not properly roll back the changes they made to the scene leaving it like "half-processed", and sometimes with gruesome results until the user hit undo.
So my solution to that problem was to turn the Scene into something resembling a persistent data structure, cheap to copy around, but not immutable because there was like a million lines of code written to mutate scenes. And I couldn't change the command API which looked like this:
bool (*execute)(Scene* scene);
... because that would break backwards compatibility in our plugin API. But after making scenes cheap to copy, I was able to do this in the client code:
Instead of executing the command on our original scene, I made a copy of it and passed them the copy. And now we were able to execute commands in parallel in different threads without worrying about thread safety (at least with respect to scene mutations). We didn't have to use thread syncs that made the renderer become unresponsive. The progress bar would stay smooth and responsive no matter what. If the command ran into an exception and returned
false for failure, we could just toss away the "half-processed" scene instead of keeping it, and we were able to now tell command writers that they didn't even have to bother to roll back their side effects to scenes on encountering an error, since we'd just avoid keeping their changes to the copy of the scene they were modifying.
Things of this sort. For all practical purposes those types of benefits you get with immutability and pure functions I got a lion's share of at a very central and useful level just doing that without having to make the entire scene design immutable (though in later versions I actually did this in a new software). So that wasn't protecting us at the level of the "class" with compiler safeguards, but it was giving us a lot of the benefits you get in terms of the particular "instance/object" of note (our central scene) in preventing it from being mutated in ways that would interfere with thread safety and things of this sort for the simple reason that our architectural design was no longer passing references/pointers to it around to be mutated even though it had a mutable interface design. And that safe guard was enforced by architectural design rather than class design.
And I'd zoom out and see if you can use a practical and pragmatic sort of solution that way if you find yourself fighting the language. If I were you and wanted to store these things in containers that require them to be copy assignable, then I'd just make it copy assignable and try to enforce, at the architectural level, some uniform design that prevents the most relevant instances of central, persistent sorts of objects from being mutated in ways that throw havoc to your ability to correctly multithread and reason about the correctness of the codebase.
Class vs Instance
What I'm basically saying is that it would definitely be nicer if every single instance of an object is immutable with that level of compiler safeguard at the whole level of the class, but you might be able to get the lion's share of the benefits by just preventing the most relevant instances of said object from being mutated (the ones that would otherwise get passed around and mutated in ways that really complicate things if we passed their original versions around by ref/pointer). At least I find this relaxed approach a lot more easy and practical to apply in a language like C++.
I might have come off in the above like I'm missing a lot of the more nuanced benefits of immutable interface designs, and I hope I don't come across that way since I still favor them when I can.
But I think copy assignment is not so bad to provide because you can easily make sure a certain object you pass is not mutated in spite of being copy assignable by merely passing a const reference, e.g., and if that's the only mutation that can occur, then copy assignment is easy to implement in terms of copy ctor, and that still leaves your parameterized constructor(s) as the only places that need to do any validation since they're the only ones that can give your object any unique state combination.
That's not a bad compromise as I see it at all in a language like C++ where, even if you managed to make every class you design immutable, people could easily make their functions cause external side effects and throw havoc to thread safety by using other objects, accessing/mutating function-scoped statics, globals, etc. We can only encourage such safety rather than enforce it with iron hammer, but minimizing mutating methods in your most central class designs and combining that with an architecture that unifies lots of things (systems, commands, evaluators, what not) and does not pass them parameters they can mutate goes a long way there.
That said, I do have a dreamy feature to help us enforce such things with an iron hammer with a sort of
pure keyword, like:
int f(...) pure;
And that would be different from const methods and applies to both methods and free-standing functions, and would be far more restrictive, preventing the function from declaring/defining statics, preventing it from accessing anything outside of its immediate scope, forcing all of its own parameters to be const, preventing it from being able to call any other functions/methods not likewise marked as
pure with the same restrictions (unless, perhaps, the local object is declared as
mutable for exceptional cases like local data structures just temporarily needed to compute the function's output) -- something to this effect (I haven't worked out the kinks in my head). It's actually my most dreamy language feature these days.