It's the small things in programming that can make a huge difference. It always felt like I should be using the C++ way because it was slightly dry-er and looked nicer, and I could have all these OO features.
Like when you see `int get_numer(void *r)` or Python's `def get_numer(self)`, or Go's `func (rational *Rational) GetNumer() (n int)`, you think: "how silly, why does the object pointer need to be in scope, just use `this`". But this tiny thing is liberating. It allows you to see that everything is just functions operating on data. And that a "method", is just a function that is taking _the entire object_ as a parameter...and hence a dependency. Which allows you to think: hmm, does this function really need to depend on the entire object...maybe it can be a separate utility function all by itself without any connection to the class. And maybe it doesn't actually need access to any of the other functions in the class...and maybe the class could be split up...etc.
I just watched a [talk](1) by Alan Kay the inventor of SmallTalk and the phrase "object-oriented" who never stops shitting on C++.
Yet OO is still absolutely everywhere.
What you are saying certainly applies neatly to getters and setters, but consider that classes are used for far more complex use cases than as a simple data holder. Indeed, if all you need is to store two integer components, you might not need a class.
Of course, in Smalltalk and it's ilk, you only have objects. So there is no opportunity to access data from within a structure (externally) without passing messages.
Dynamic dispatchinga are not necessarily evil, but that also doesn’t exclude C. Not sure how this idea C++ is inherently more dynamic or static comes from. For a ling time you can implement all the dynamic behavior of C++ in C, and it took C++ ‘s template as expressive as C macros.
Sometimes, less is more.that’s where C++ never gets right.
If you want to do generic programming, it's much better to write f(a, b) instead of a.f(b).
Suppose I'm writing a generic algorithm that needs some function distance(A a, B b) for generic A and B. Distance is probably a commutative function, why would a.distance(b) be better than b.distance(a)? It's arbitrary.
Secondly, if functions are first-class citizens, and I don't "own" the types A and B, it's much easier to implement distance(A a, B b) myself than it is to extend the types A and B with a member function `distance`.
Effective C++ (by Scott Meyers) Item 23:
“Prefer non-member non-friend functions to member functions. Doing so increases encapsulation, packaging flexibility, and functional extensibility.”
The text has a lot more detail but that’s a brief summary. Folks just need to read the literature then this sort of knowledge would be in common use.
One downside of language evolution is a lot of people focusing on new language features, etc, but then some of this older, important knowledge gets skipped over.
OOP is just functions that are only polymorphic in 1 argument, when you want it on all of them.
The best paradigm in programming has to be procedural style. The cleanest code I’ve written and read has always been procedural. I even maintain a monster VBA Word Macro at work and - after some much needed refactoring - its pretty easy to understand (ignoring VBA’s warts).
The flow of logic and dependencies on data is much easier to follow in procedural.
Functional stresses composition so functions should stay small, and ADTs allow for all effects (errors!) to be represented. Strong typing helps reduce the number of tests I need to write, as well.
I recently watched a talk on just what precisely Functional Programming is, and the whole time I just thought "wait, doesn't everyone do this? Isn't this just the overwhelmingly obvious way to write code?" Around the same time, I found myself running into some very OO projects, and simply couldn't understand the actual reason for all of the ceremony and ridiculous amounts of encapsulation. And this wasn't in Java, which of course is infamous for its seemingly infinite amount of ceremony -- this was the documentation of a widely-used GUI library for Python.
Like, to my mind, classes are useful when you need to create objects whose values are unknown, i.e. the analogy where the class is a "cookie cutter" which creates the cookies. Classes are perfectly fine for that use-case but by no means should it be that which forms the backbone of your project; my (admittedly limited) view is that stand-alone functions are far and away the lowest-overhead and reliable device for composing parts of a system.
I really get the feeling that OOP is sort of like how synths were in the 80s -- everyone just kinda lost their minds for a second and used them everywhere, whether it made sense for the music or not. But from what I can tell, OOP just isn't really the right paradigm for a whole bunch of projects, and often the design of the code vs the design of the thing feels like ramming a square peg into a round hole.
Edit: all that said, if the above smacks of the Dunning-Krueger effect, I'd love to be made aware of that.
package hello
type cat int
func greet(c cat) cat {
return c + 1
}
type dog string
func greet(d dog) dog { // greet redeclared in this block
return d + "one"
}
but you can with methods: package hello
type cat int
func (c cat) greet() cat {
return c + 1
}
type dog string
func (d dog) greet() dog {
return d + "one"
}The biggest benefit is the structure, you get an API that can control any aspect of the app out of the box without the need to redeclare stuff.
I am so much used to the idea of swapping out functionality (set of functions and a state). For example, just recently I had to swap out an Excel spreadsheet reader/writer library in Go for performance reasons, and would have bled a lot (due to refactoring) had it not been encapsulated as an interface.
Interfaces are much better. What's the difference? It's this: no inheritance.
Still, with interfaces your complaints remain unsatisfied. But add generic functions and then you can have the mix of OO-like and not-OO-like APIs you have in mind.
Composition? AssignShiftLeftExpression has-a AssignOpExpression which has-a BinaryAssignExpression which has-a BinaryExpression which has-a UnaryExpression which has-a Expression which has-a Node?
return this.assignOpExpression.binaryAssignExpression.binaryExpression.unaryExpression.expression.node.op;
or return this.op struct foo {
...
};
struct bar {
struct foo super;
...
};
You can now safely pass a pointer to bar into a function that takes pointer to foo.Generally true, but not guaranteed by the standard: https://stackoverflow.com/questions/1241205/
> "All pointers to structure types shall have the same representation and alignment requirements as each other."
And so the concern wouldn't apply to this pattern?
> all|every|always ... in C.
But once your expectation is that you have to do all the work at the FFI boundary it's less frustrating than to experience all the small mismatches as compiler errors or annoying runtime errors.
Of course the issue for C++, unlike the other examples here, is that it does not have a runtime layer that can be used to bridge the gap. So we either write a wrapper in C++ using extern C, or use a tool to do it.
It seems there was a GSOC effort for SWIG to generate C wrappers for C++ libs but it might not have made it all the way? I don't see C as a target language on SWIG's site.
Still, a bespoke, high level design for the C wrapper is always going to be less painful for the consumer.
C++ is not a superset of C. C99 has and C++ (at the time of writing) doesn't have: restricted pointers, designated initializers, variable length arrays, and probably more. These are language features that are actually used.
This doesn't change any of your core points.
neither, per standards, does c
There is no reason to use void*, create a distinct type which can be opaque if you want, and then you can hide the implementation details in the C++ implementation to call through to the C++ classes. You get some degree of type safety this way.
struct Rational2;
struct Rational2 *make_rational(int,int);
Then in the file: Rational2 *make_rational(int n,int d){
return (Rational2 *)new Rational(n,d);
}
void del_rational(Rational2 **pp){
delete (Rational2 *)*pp;
*pp=nullptr;
}
And so on. You could probably arrange for it to be called Rational in both languages, starting out along these lines and then taking it from there: #ifdef __cplusplus
class Rational { ... };
#else
struct Rational;
typedef struct Rational Rational;
#endif
And now you can could your C helpers from C++ as well, and the result is a genuine C++ Rational object that you can use either way. I don't think the ODR applies across languages, and I'm not 100% certain this would actually be an ODR violation anyway, at least not quite, but you'd need to ask somebody more qualified than me.(Another suggestion I would have is to bracket the entire header in the ifdef'd extern "C" {...} block, which limits the amount of extra crap you have in the header and per function. I think you can direct clang-format not to indent these blocks.)
The allocation and the opaque handler can still be useful for ABI stability purposes, but that's true in C++ as well.
I didn't supposed that anything really meaningful can come...
I wonder if placement new would run into the same linker problem that the article mentions -- I'll have to try it at some point :)
Instead write a higher level module in C++ on top of the C++ library which implements some of the "application logic" and only exposes a very small app-specific (and non-OOP) C API to the rest of the application.
For instance with Dear ImGui I often write the entire UI code in C++ and then only expose a minimal C API which connects the UI code to the rest of the application (which is written in C).
Same with managing C++ object lifetimes, let the C++ side take care of this as much as possible, and don't expose pointers to C++ objects to the C side at all.
Unfortunately these seem to be quite rare (Dear ImGui is such a well-designed C++ API, and I actually also use the code-generated cimgui bindings more frequently now in my projects (haven't tried the 'new' official bindings yet). Interestingly the Dear ImGui C++ API is much closer to a typical "flat" C API than a typical class-based C++ API (Dear ImGui is mostly just a flat soup of functions wrapped in a namespace and with some mild overloading).
You could just use new() in the first example instead and avoid the whole issue.
I wrote and shipped C++ as a job for many years -millions of lines I'm sure- and I've 'reverted' to plain C around 2007 or so, and I couldn't be happier.
I did too, all the way up to (IIRC) 2011. My happiness levels improved when I stopped working on C++ and, for the type of native-code problems that I used C++ for, used plain C instead.
My goal is to ship code, with a trade-off between delivery dates, runtime performance and robustness. As the problem gets more complex, I find that C++ muddies the waters even more, impacting on all three axis' above.
It's substantially easier to bind FFI to it if it's usable from C.
If it's usable from C, it's usable from umpteen other languages.
Thou shalt not write a C++ component without a C API.
Both Rust and Zig for example nailed it