IMO before the before and after patterns are a mess compared to a struct in Rust etc or a dataclass in Python. `this` or `self` is important to be explicit; C++ is an interesting case in that it gives you a choice; `this->` is the better approach than implicit, which is ambiguous.
I can't tell from a glance from either before or after what the fields of the class are, let alone what sort of data goes in them. I suspect my IDE would have a struggle as well, and allow mistakes, at runtime, to either raise exceptions, or perform incorrect behavior.
I think maybe a cleaner approach in this domain may be TS interfaces + standalone functions that act on them.
There’s no real thing preventing #1 from being done in C++, but it is incredibly hard to remove bad patterns from a language that allows them. This is both problematic because these languages don’t provide first class support to remove unwanted features, and because huge amounts of existing code will use patterns you’re hoping to excise. In order to successfully do this you have to do a huge amount of both linting and dependency checking, and the resulting requirement that you not use any existing libraries makes using Result in C++ about as hard as doing a progressive rewrite into Rust.
One thing I find problematic about C++ style OOP is that classes are just bags of global-like variables. Unless you keep your classes very small, member data can be mutated in a dozen places in the class in a dozen ways, and it's just a big spaghetti mess. I understand that c++ code can be written cleanly and readably, but in most cases it's not.
My dad has had trouble understanding classes for decades, but he had mostly stopped programming during that timeframe as well so it wasn't something he was going to put much time into learning. Now, he's returned to programming more regularly but still is having trouble with classes. I figured the big problem for him is exactly what the problem for you was, the hidden this pointer.
I'd started working on manually writing the same code is both C++ and C, but your approach of using something automated is an even better idea. Showing the implicit this pointer isn't hard to do manually, but polymorphism is a bit more of a pain. But I think the best part about using a tool is that he can change the C++ code and see how it affects the emitted C. Being able to tinker with inputs and see how they affect the output is huge when it comes to learning how something works.
That sounds like a game of Tetris though, with or without IDE support ;) It would have been better if C++ treated structs like C structs, not like classes, but too late for that I guess.
Thanks for the explanation. Self referential structures are indeed problematic.
I'm actually pretty sanguine about the general case of invalidating C/C++ patterns. It doesn't surprise me that some new computer science is going to be necessary to help deal with the edge cases exposed by mainstreaming a new paradigm.
One problem in C++ is if you define a struct then you have to define a bunch of operators yourself (operator<, etc.) which gets annoying after you've realized you've needed them a bunch of times.
I mean, it's possible to write bad code in C++. But at least it's possible to write good code too. C++ makes a careful sequence that's hard to get wrong and easily compiler enforced:
* if a given subobject (field or base class)'s constructor runs, its destructor is guaranteed to run.
* in particular, in low-level ownership classes, you can arrange for field initializers to be `noexcept`, so you get to run their dtor, regardless of subsequent manipulation of the fields - just be sure to not assume invariants from the fully-constructed main object case. In most classes, deferring all ownership logic to the fields is simpler - rule of zero beats rule of 5. And if you do add manual try-catch during construction it will actually work.
Languages other than C++ generally fail in several ways:
* Allow exceptions to be thrown by the runtime for reasons unrelated to the code being executed
* No support for subobjects, forcing all objects to be allocated separately and thus the runtime not knowing it needs to poke them since finalizers are only applied to objects not pointers. In particular, Python's `contextlib.ExitStack` can narrow the window in which case leaks are possible, but not eliminate it.
Rust does slightly better than C++ by enforcing trivial moves, at the cost of making many useful programs impossible to write.
I spent maybe ~15 years writing C++ over hobby + career timespan. Was a huge proponent of the language but spent the last 2-3 years over in Java land while doing all my greenfield work in Rust.
Came back to another lower level project written in C++/0x14 lately and it's a bit of a shock how my things I 'forgot' even on a modern codebase. Things like implicit constructor conversions, the fact that the type inference is pretty weak still(why can't I just construct unique_ptr w/out the type when I pass it direct[1]). Headers and package management is a total mess.
At this point unless I've got a hard constraint(like QNX or other yet supported platforms) I'm going to reach for Rust just about every time.
[1] Yes I know make_unique() is a thing but it's a C++/0x17 and just patches one part of type inference issues.
You should take a look at the Concepts TS - if I understand what you're trying to achieve correctly then Concepts are the approach C++ is taking to address these types of issues.
I had a roommate in college who would write code like this. I (and several other people) tried to explain to him that he couldn't necessarily count on the pattern working all the time. His response was to say, "But it works here!" and continue abusing C++'s undefined behavior.
I stopped giving him help with his CS coursework in fairly short order.
I'm all for proper rants against popular tools to keep people on their toes.
This isn't one of those.
"Objects bind functions and data structures together in indivisible units. I think this is a fundamental error since functions and data structures belong in totally different worlds."
Sure—a class defines a type and operations on that type. What's fundamentally wrong about date.addDays(1) vs. date_add_days(date, 1)? (Let's skip the mutable state argument and assume both versions return a new date.)
There is the problem that sufficiently opaque classes are hard or impossible to extend. That's the class author's fault: this is an avoidable problem in every object-oriented language I've used.
"Functions are understood as black boxes that transform inputs to outputs. If I understand the input and the output then I have understood the function. … Functions are usually 'understood' by observing that they are the things in a computational system whose job is to transfer data structures of type T1 into data structure of type T2."
A constructor is a black box that converts a data structure of type T1 into a data structure of type T2. Objects just also have other black box functions defined on them.
Sure, some objects are stateful, but they don't have to be.
"In an OOPL I have to choose some base object in which I will define the ubiquitous data structure, all other objects that want to use this data structure must inherit this object."
Um, no. This is a job for composition, not inheritance.
"Instead of revealing the state and trying to find ways to minimise (sic) the nuisance of state, they hide it away."
They hide state's implementation, for mutable objects.
Sure, an allocated piece of memory might have grown, or even moved. Why should I care? I still see the state I care about, presented through a hopefully useful abstraction.
This rant seems to somehow miss the points of both object-oriented and functional programming, instead harping on mostly meaningless (or outright wrong) details. Or am I missing something here?
C++ doesn't have the tool to do so. Just like you can build abstraction for raw data in any language, but only the compiler in static typed language will scream at you at compile time if you use it the wrong way and dynamic language will just believe you will do the right thing.
Yeah that's was just the first public C++ library with this pattern that popped into my head. I just make all my classes final out of habit and don't think about it. I remove final if I want to subclass, but that almost never happens.
I agree with most of your points, but I wanted to also point out that you don't need C++'s ctor/dtor spaghetti to have useful RAII: Rust achieves it without even having first-class constructors (and opt-in custom destructors via Drop).
A lot of C++ changes are aimed at this. For example C++11 auto removed a lot of the ugliness, and is removing even more with auto template parameters and deduced return types.
- how do you implement reflection without either ugly macros or a precompiler?
- how do you implement metaclasses without a meraobject ? (e.g. how would you list all the methods or properties of a class)
- how do you implement a tree structure where the parents knows its children and the children know its parent in modern C++ (or Rust without unsafe :-)) ?
I can't tell from a glance from either before or after what the fields of the class are, let alone what sort of data goes in them. I suspect my IDE would have a struggle as well, and allow mistakes, at runtime, to either raise exceptions, or perform incorrect behavior.
I think maybe a cleaner approach in this domain may be TS interfaces + standalone functions that act on them.
reply