[HN Gopher] Can you use a class in C?
___________________________________________________________________
Can you use a class in C?
Author : D4ckard
Score : 76 points
Date : 2023-08-12 07:05 UTC (1 days ago)
(HTM) web link (d4ckard.github.io)
(TXT) w3m dump (d4ckard.github.io)
| D4ckard wrote:
| Hey , first time posting something I've made here I think :D. I'm
| excited to hear what you think about it!
| lmz wrote:
| I'd suggest removing the part where you use malloc to allocate
| and just skip to the part where you use new and link to
| libstdc++. At the malloc part I was wondering what the point of
| the whole thing was when you're basically using none of the C++
| logic and rewriting everything in C.
| rezonant wrote:
| Also the Rational usage example in C++ is using stack
| allocation. It's weird to then pretend like heap allocation
| is equivalent in the C example. I think making stack
| allocation work in this case would probably be difficult, but
| it's worth pointing out in the article that the choice is
| intentional and not a misunderstanding of non-new allocation
| in c++.
|
| You could just use new() in the first example instead and
| avoid the whole issue.
| buserror wrote:
| The only times I had to do that I.... converted the whole heap of
| C++ to C and used that instead. You rarely see libraries using
| all the most complex constructs of C++, and quite frankly, if
| they do, I'd rather stay clear of that pile of bloatware :-)
|
| 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.
| lelanthran wrote:
| > 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.
| tragomaskhalos wrote:
| Your allocation method - malloc + cast - may work for simple
| classes but overall it's a serious no-no; think you should either
| be using new/delete, or, if sticking with malloc /free, use
| placement new and an explicit destructor call respectively.
| jwatzman wrote:
| The article discusses constructors and how to make them work
| under "Linking the C++ standard library".
|
| 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 :)
| krater23 wrote:
| And the stupidest thing, he uses this malloc in a .cc file,
| where it would no issue to just use new. Thats the point where
| I stopped reading. After auto main() -> int { ... }
|
| I didn't supposed that anything really meaningful can come...
| flohofwoe wrote:
| IME the best way to wrap C++ libraries for use in a C code base
| is to move at least one step higher then just wrapping every C++
| class method with a C function (because this will result in an
| absolutely miserable C experience - you're basically writing C++
| code in C, but without all the C++ syntax sugar).
|
| 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.
| rezonant wrote:
| I agree, with the caveat that given a heavy duty, large, and
| well-understood and -designed C++ API, the tradeoff of using a
| tool-generated wrapper might be worth it, even if such a
| wrapper may be more obtuse than one specifically considered by
| a human developer.
| flohofwoe wrote:
| > well-understood and -designed C++ API
|
| 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).
| rezonant wrote:
| Very true. Designing the C API with intention can be a way
| to fix the mistakes sometimes :-)
| cesaref wrote:
| I think you can do better than this void* type this equivalent -
| if you consider what is happening with FILE* when you use
| stdio.h, you have basically a class interface, and i'd follow
| this pattern.
|
| 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.
| tom_ wrote:
| Indeed. It's super easy to create an opaque type in C: forward-
| declare a struct. So in the header: 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.)
| zabzonk wrote:
| it's super easy to define an opaque type in c++ - you do it
| just like you do in c. you don't need to jump through these
| hoops.
| tom_ wrote:
| Quite. And in fact, in this specific situation, in C++ you
| don't need to jump through any hoops at all, because you
| have the Rational class there already, ready for use. This
| whole business only exists to provide a C-friendly wrapper
| for this existing C++ class, along the lines of the one
| proposed in the article, with a couple of tweaks that I
| think would improve it.
| gpderetta wrote:
| The converse is that if your class is just a standard layout
| object with a trivial destructor, you can expose an
| equivalent C definition without all the C++ sugar and avoid
| the forced allocation.
|
| The allocation and the opaque handler can still be useful for
| ABI stability purposes, but that's true in C++ as well.
| [deleted]
| mtlmtlmtlmtl wrote:
| Because all pointers are the same size in C, and a pointer to a
| struct always points to the first member of the struct, you can
| do inheritance by having the "superstruct" be the first member of
| the substruct: struct foo { ... };
| struct bar { struct foo super; ... };
|
| You can now safely pass a pointer to bar into a function that
| takes pointer to foo.
| jjgreen wrote:
| _all pointers are the same size in C_
|
| Generally true, but not guaranteed by the standard:
| https://stackoverflow.com/questions/1241205/
| commonlisp94 wrote:
| In modern times, this only applied to MSDOS
| rezonant wrote:
| Particularly function pointers may have different sizes than
| what you'd expect. Thus it's best practice to always use
| sizeof() for the specific type of pointer you are interested
| in if you need to know it at runtime just as with any normal
| non-pointer type.
| michaelcampbell wrote:
| I've been out of the c/c++ game for a long time, but it's
| always interesting to see what the edge, or non-guaranteed,
| elements are going to be whenever anyone claims:
|
| > all|every|always ... in C.
| mtlmtlmtlmtl wrote:
| Right, my bad. It's really struct pointers that are
| guaranteed to be the same size.
| isidor3 wrote:
| From that same post, though, referencing the C 11 standard
|
| > "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?
| [deleted]
| crabbone wrote:
| Traditionally, pointers to functions were an exception. But
| I haven't red the new standards. I just assume pointers
| aren't all the same size, and don't try to rely on their
| size.
| masfuerte wrote:
| This is kind of what Microsoft's COM gives you. You have to write
| your classes in a particular way but then you get a well-defined
| API that can be consumed in many languages, including C.
| WhereIsTheTruth wrote:
| class is a mistake, all C need is proper union (tagged union)
|
| Both Rust and Zig for example nailed it
| hwc wrote:
| why not forward decare a struct type in `ifndef __cplusplus` and
| use pointers to that type instead of `void*` pointers?
| krater23 wrote:
| He uses malloc instead of new in a .cc file. I don't think that
| he realized that all what he is doing is overcomplicated at
| all.
| sharikous wrote:
| My philosophy on this is to treat C++ and C as completely
| different languages (as they actually are), like say Java and C
| or python and C. Yes it's nice that some part of the header file
| can be parsed in both languages and that most of the syntax is
| similar.
|
| 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.
| rezonant wrote:
| As with all of the languages you mention, consuming C from the
| other language is trivial. And since C++ is a superset of C
| that is certainly true there. Consuming the other language from
| C is difficult without a C compatible runtime API to help
| bridge the gap.
|
| 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.
| kaashif wrote:
| > And since C++ is a superset of C that is certainly true
| there.
|
| 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.
| patrick451 wrote:
| C++ has had designated initializers since c++20.
| turndown wrote:
| Any look into this will show that C's designated
| initializers are significantly more flexible/better in
| every way.
| SubjectToChange wrote:
| It's amusing to me that whenever C has a feature that C++
| doesn't, C programmers hammer on the usefulness and power
| of said feature, but on the converse those same C
| programmers hammer on the divine simplicity of C.
|
| C++ has constructors, it doesn't really _need_ designated
| initializers in the first place. They were added
| primarily for C compatibility.
| kaashif wrote:
| Point taken, I was inaccurate. Out of order designated
| initializers still don't work in C++20, right? Which
| still means C++ isn't a superset of C.
| ftaghn wrote:
| > variable length arrays
|
| It was such a terrible feature it was made optional in the
| C11 standard (you can be a conforming C11 compiler and not
| allow this feature) and will never, ever be implemented in
| a Microsoft compiler (while C is not a priority for MS, do
| note that they updated to C11 and C17).
|
| You can hear their reasoning there :
|
| https://devblogs.microsoft.com/cppblog/c11-and-c17-standard
| -...
|
| The linux kernel used to make use of the feature and
| removed every instance of it from the code base :
|
| https://www.phoronix.com/news/Linux-Kills-The-VLA
|
| > Particularly over the past several cycles there has been
| code eliminating the kernel's usage of VLAs and that has
| continued so far for this Linux 4.20~5.0 cycle. There had
| been more than 200 spots in the kernel relying upon VLAs
| but now as of the latest Linux Git code it should be
| basically over.
|
| While I do agree that it is wrong to consider C++ a
| superset of C, it is time to forget about C99's biggest
| mistake and treat it as if it didn't happen.
| kaashif wrote:
| Agreed completely on all counts.
|
| Never ever use VLAs.
|
| My point is really just semantic, the overall argument I
| was responding to is intact.
| zabzonk wrote:
| > 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
|
| neither, per standards, does c
| rezonant wrote:
| Yes, but C is simple (and standard) enough that higher
| level runtimes have more or less universally managed to
| construct decent FFI mechanisms to access it.
|
| C++ FFI would be kind of feasible if mangling were
| standardized (for instance) :-/
| SubjectToChange wrote:
| Standardizing name mangling would only be a baby step
| towards C++ ABI compatibility, sadly.
| FreshStart wrote:
| Yes, but the performance penalty becomes glaringly obvious as you
| are constantly dereferencing (composition, relationship and
| function) pointers and thus generate misses..
| grumblingdev wrote:
| It took me a long time to realize that I actually prefer not
| using classes.
|
| 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.
|
| [1]: https://www.youtube.com/watch?v=oKg1hTOQXoY
| cryptonector wrote:
| Nowadays OO is seen as a mistake.
|
| 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.
| danhau wrote:
| Thank you. You put into words what I was thinking.
|
| 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.
| thewix wrote:
| I prefer functional. Procedural is not restrained enough and
| allows for mutable global state. This can go off the rails
| when multiple people are in the code.
|
| 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.
| diarrhea wrote:
| I think ADTs and effects system are orthogonal though. A
| "print" has an effect but I don't know of languages where
| failure of that is propagated (for example in Rust, println
| doesn't evaluate to Result).
| bluGill wrote:
| OO does not come from Smalltalk. Simula was the inspiration for
| OO in C++.
| 38 wrote:
| you cant reuse names with top line functions:
| 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"
| }
| diarrhea wrote:
| I don't think that's a strong argument. It's just function
| overloading that some languages enable, others don't. Not a
| categorical difference.
| 38 wrote:
| OK please show me the equivalent code in C then
| leeman2016 wrote:
| How would you do DI abstractions using just functions? (Honest
| question)
|
| 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.
| shortrounddev2 wrote:
| Function pointers in a struct
| winstonewert wrote:
| How would either approach be different? In both cases you
| would have to rewrite the code that directly interacts with
| your Excel library.
| yCombLinks wrote:
| And a well defined class defines the structure and valid
| permutations for that data in a clear and easy to find place.
| billfruit wrote:
| And allows to create any number of objects of its kind
| without explicitly needing to allocate memory for the
| objects. That is a major advantage I think, without that the
| code will get cluttered with all the memory
| allocation/deallocation going on explicitly.
| Waterluvian wrote:
| This is why I like Rust's associated functions concept. It is
| just functions and data. But with organization of clear
| relationships. "These functions act on these data." (And a bit
| of syntactic sugar)
| diarrhea wrote:
| One disadvantage here is taking the entire struct on each
| operation even if only a single part of it is needed. That
| can be much harder to deal with due to ownership rules (if
| &Mut self or all of self is taken). Free functions can be
| easier then.
| stabbles wrote:
| Also Bjarne Strostroup said that this (pun intended) was a
| mistake, since often there is no "most important" object.
|
| 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`.
| commonlisp94 wrote:
| It would make a huge difference if that style were used more
| in practice.
|
| OOP is just functions that are only polymorphic in 1
| argument, when you want it on all of them.
| coldtea wrote:
| Multiple dispatch for the win...
| bobboies wrote:
| Also see:
|
| 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.
| mckravchyk wrote:
| I have tried to sell myself on using functions (not pure
| functions though but ones that accept an object and modify it)
| but ultimately sticked to OO. I try to find the balance in-
| between, if something is processing heavy it's going to be a
| function, but if it does not do much by itself and relies on a
| lot of state, it's going to be a method in a class - which may
| call a function or a few.
|
| 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.
| rezonant wrote:
| Alan Kay isn't going to agree that you should simply pass
| structures around and use element-level procedures. Message
| passing as a concept is a higher order version of what you see
| in C++, not what you see in C. A big reason Kay dislikes C++ is
| because it doesn't go far _enough_ in terms of dynamic dispatch
| and the ability to substitute implementations dynamically at
| runtime. One way to put it is: C++ makes decisions at compile
| time that should be made at runtime.
|
| 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.
| chadcmulligan wrote:
| Indeed, and C++ was initially a preprocessor (CFront) by
| Stroustrup, dynamic dispatch was considered too slow at the
| time, so c++ style objects and methods were faster and as a
| faster version of simula [1]. Objective C was an alternative
| that used something closer to smalltalk but it was slower and
| a few years later. C++ was first released in 1983 internally
| at AT&T, and for perspective, the 286 was released around the
| same time with a clock speed of 8 MHz.
|
| [1] https://en.wikipedia.org/wiki/C%2B%2B
___________________________________________________________________
(page generated 2023-08-13 23:01 UTC)