8 ms·
The first chapter, on abstraction and information hiding, is probably the most important. Every programmer should know how to do it. It's one of C's main streng
by yawaramin 9y ago
The first chapter, on abstraction and information hiding, is probably the most important. Every programmer should know how to do it. It's one of C's main strengths that it allows abstract data types so easily. ADTs in fact allow better binary compatibility since you only expose pointers as your public API, not structures of different shapes and sizes.
I'm continually surprised by how often people insist on putting struct definitions right in their header files, defeating the whole idea of type abstraction.
- Koshkin 9y ago> It's one of C's main strengths that it allows abstract data types so easily. On the contrary, data type abstraction in C is a pain. This was one of the reasons for creation of "C with classes" (a.k.a. C++).
- catnaroek 9y agoNot that C++ is particularly good at data abstraction either.
- ASalazarMX 9y agoI prefer to call it "C Gone Wild".
- dangerbird2 9y agoThe only things that are particularly painful about opaque types in C is that you can't easily use stack-based memory to allocate opaque objects. This of course is true for C++ opaque types, but people tend to rely on private fields of concrete types instead of "PIMPL" struct declarations, even though the latter is arguably better for binary compatability and information hiding.
- catnaroek 9y agoNot that object-oriented languages are particularly good at it, but defining and using abstract data types in C is a pain: (0) Forward declarations are the only way not to explicitly reveal the representation of types, but you have to pay an indirection tax for it, whether you actually need indirection or don't. (1) Without heavily abusing the preprocessor, there is no way to express the fact that two abstract data types expose the same interface. (2) Again, without heavily abusing the preprocessor, there is no way to make common algorithms work on abstract data types that have a common interface.
- yawaramin 9y ago(0) True, but the default assumption really should be that the indirection tax is fine. There are plenty of non-realtime C apps. If you're really seeing a slowdown, then by all means trade off some of that abstraction power. (1) & (2) You could use conversion functions, no? E.g.: typedef struct ordering_t* ordering; typedef struct person_t* person; typedef struct business_t* business; ordering ordering_of_person(person); ordering ordering_of_business(business); void* sort(void*, ordering);
- catnaroek 9y ago(0) The indirection tax is not fine. I want both nice abstractions and efficient implementations. (1) and (2) Your proposal doesn't work, because different abstract data types are, well, different, even if they expose the same operations. (Perhaps you're confusing “abstract data types” with “abstract classes”?) If you don't believe me, try merging a binomial heap and a pairing heap.
- jcelerier 9y ago> Forward declarations are the only way not to explicitly reveal the representation of types, but you have to pay an indirection tax for it, whether you actually need indirection or don't. How would you solve this while still allowing object files to compile like they do ? e.g. if I have a .c file with a function void f(some_type t) { ... } the compiler has to know at that point what is the size of t so that it can allocate the correct space on the stack at the beginning of the function when compiling it. You could fix this with some kind of module system but this would mean that you'd always need the whole module which may be multiple megabytes vs a .hpp which is generally small (or a forward-declaration which is negligible). Regarding 3, C11 has some generics nowadays: http://en.cppreference.com/w/c/language/generic http://en.cppreference.com/w/c/language/generic so for simple stuff it can be done (and for more complex stuff you'd be much better served by c++ templates)
- catnaroek 9y ago> You could fix this with some kind of module system but this would mean that you'd always need the whole module which may be multiple megabytes (...) You don't need the whole module. You need the module's interface to compile anything that depends on the module. Of course, type sizes and alignments must be a part of the interface in a C-like language. But “exporting sizes and alignments” doesn't necessitate “exporting the entire internal representation of the type”, cf. Rust. > (...) vs a .hpp which is generally small (or a forward-declaration which is negligible). You can't forward-declare type sizes and alignments. This is precisely why you must pay the aforementioned indirection tax. > Regarding (2), C11 has some generics nowadays Which is just a compile-time switch statement on types. You still have to write separate implementations of generic algorithms for every concrete type on which you want to use them. > (and for more complex stuff you'd be much better served by c++ templates) The pain associated to using C++ templates is exactly what happens when you try to do generic programming in a language with no support for abstract type specifications.
- megous 9y ago> I'm continually surprised by how often people insist on putting struct definitions right in their header files, defeating the whole idea of type abstraction. It also defeats the idea of easy struct embedding as is done for example in the Linux kernel. Different codebases, different needs.
- sparkie 9y agoYou need to define a struct in a header to pass by value, which is usually cheaper than having to dereference a pointer (often resulting in a cache miss). The style presented in the paper has several pointer dereferences, for example each type has another pointer to a class header which contains its constructor and some other function pointers. I personally do not like the style in this paper as it makes APIs difficult to read - you need to resort to documentation to understand anything. In the first set example: #ifndef SET_H #define SET_H extern const void * Set; void * add (void * set, const void * element); void * find (const void * set, const void * element); void * drop (void * set, const void * element); int contains (const void * set, const void * element); #endif What type is add expecting and what is it going to return? We have no clue, other than to continue reading the docs. A "better" approach to an opaque pointer type is to simply declare a struct in the header, but only define it in the implementation file. The above API becomes this, where it's pretty obvious from the arguments and return types (it also avoids having to do an explicit manual cast in each of the methods): #ifndef SET_H #define SET_H #include "Object.h" typedef struct set_t Set; Object* set_add (Set*, const Object* element); Object* set_find (const Set*, const Object* element); Object* set_drop (Set*, const Object* element); int set_contains (const Set*, const Object* element); Set* set_alloc(void); void set_free(Set*); #endif Obviously, this doesn't play well with the full OOP approach this paper takes, but IMO, it's better to just compose structs using the style above, and leave memory management up to the type rather than trying to have a fancy global "new" and pointers to constructors.
- dualogy 9y ago> You need to define a struct in a header to pass by value, which is usually cheaper than having to dereference a pointer (often resulting in a cache miss). If one wanted to do this, what kind of struct size would be the cut-off point (on current-gen machines) where passing the reference instead of a full struct copy becomes advisable? (For funcs where both would work equally fine)
- comex 9y agoABIs tend to implicitly pass by reference anyway for structs larger than two words or so. (For example, in the AMD64 ABI, two 64-bit words is the cutoff for both arguments and return values. On 64-bit Windows, the limit is one word for both, and on 32-bit ARM, it's four for arguments but one for return values.)
- hawski 9y agoI was thinking lately that C is more data-oriented than many. However I'm limited in knowledge mostly to imperative languages. What I mean is that defining arrays of structs has very light syntax. For example in Python AFAIK you can't easily do this. You have to use dictionaries (which is cumbersome if you have many rows) or some kind of class/module that will make it even less clear. What Suckless [1] guys are doing where you can use C as configuration language is example of it. Like keybindings configuration [2] in dwm [3]: static Key keys[] = { /* modifier key function argument */ { MODKEY, XK_p, spawn, {.v = dmenucmd } }, { MODKEY|ShiftMask, XK_Return, spawn, {.v = termcmd } }, { MODKEY, XK_b, togglebar, {0} }, { MODKEY, XK_j, focusstack, {.i = +1 } }, { MODKEY, XK_k, focusstack, {.i = -1 } }, /* ... */ }; When I do something in Python I miss this. C++ most likely will require doing some playing around with constructors. Also with C99 it's only sweeter. When you need you can use designated initializers [4] - putting names of fields or indices of array in initialization list. You can use compound literals [5] to obviate some need for constructors. [1] https://suckless.org/ https://suckless.org/ [2] https://git.suckless.org/dwm/tree/config.def.h https://git.suckless.org/dwm/tree/config.def.h [3] https://dwm.suckless.org/ https://dwm.suckless.org/ [4] https://gcc.gnu.org/onlinedocs/gcc/Designated-Inits.html https://gcc.gnu.org/onlinedocs/gcc/Designated-Inits.html [5] https://gcc.gnu.org/onlinedocs/gcc/Compound-Literals.html https://gcc.gnu.org/onlinedocs/gcc/Compound-Literals.html
- geofft 9y agoTake a look at https://github.com/python-attrs/attrs https://github.com/python-attrs/attrs which will let you do something like import attr @attr.s class Key(object): modifier = attr.ib() key = attr.ib() function = attr.ib() argument = attr.ib() keys = [ Key(MODKEY, XK_p, spawn, {'v': dmenucmd}), Key(MODKEY|ShiftMask, XK_Return, spawn, {'v': termcmd}), # ... ] which gets you the concise and readable syntax of C structs, while keeping these things as actual attributes as if they were classes (e.g., [key.modifier for key in keys] will work). And you get designated initializers via Python's usual keyword-argument syntax, the ability to specify attr.ib(default=...), and a few other things.
- rmind 9y ago> I'm continually surprised by how often people insist on putting struct definitions right in their header files, defeating the whole idea of type abstraction. In general, I totally agree with you that it is best to keep the structure definition in the translation unit (the .c file) and opaque structure declaration in the header. However, there are some cases when it is not very practical, e.g. when you you want to embed the structure into another (for performance reasons or cases like on-disk structures, etc) or when you want to separate out some logic into another .c file (think of separation of concerns), instead of having one massive .c file. When there is such need, I generally create two headers files, e.g. foo.h (for the public API) and foo_impl.h (structure definition and related stuff), with something like: #if !defined(__FOO_PRIVATE) #error "foo_impl.h should only be included by the internal foo modules" #endif
- yawaramin 9y agoGood points. It's at least possible to break up a massive .c file into multiple smaller .c files amalgamated with #includes, right? The foo_impl.h idea is a good one also.
- yoklov 9y agoNot including the struct in the header defeats the ability of the user to control where the type is allocated in memory, among other things. That’s a feature that many c programmers are willing to give up a certain degree of api/abi compatibility for. It’s just a different tradeoff, neither is the best choice in all cases. Of course, if you’re willing to put in a lot of extra legwork, you can do what pthreads does, and define dummy types with the same size for this.