defines the virtual function table for the LDAP module, and any other subsystem that looks things up via the abstract dict interface can consequently be configured to use the ldap service without concrete knowledge of it.
So does the good old Quake 2 rendering API. The game exported a bunch of functions to the renderer via refimport_t and the renderer in return provided functions via refexport_t. The only visible symbol in a rendering DLL is GetRefAPI_t: https://github.com/id-Software/Quake-2/blob/master/client/re...
I somehow suspect that the reason why Quake2 does this lies in the legacy of Quake1 written in DJGPP. DJGPP supports dynamicaly loaded libraries (although the API is technically unsupported and internal-only), but does not have any kind of dynamic linker, thus passing around pair of such structs during library initialization is the only way to make that work.
I spend a ton of time in FFmpeg, and I’m still blown away by how it uses abstractions to stay modular—especially for a project that’s been around forever and still feels so relevant. Those filtergraphs pulling off polymorphism-like tricks in C? It’s such an elegant way to manage complex pipelines. e.g.
That said, keeping those interfaces clean and consistent as the codebase grows (and ages) takes some real dedication.
Also recently joined the mailing lists and it’s been awesome to get a step closer to the pulse of the project. I recommend if you want to casually get more exposure to the breadth of the project.
I haven’t worked with ffmpeg’s code, but I have worked with QEMU. QEMU has a lot of OOP (implemented in C obviously) that is supported by macros and GCC extensions. I definitely think it would have been better (and the code would be easier to work with) to use C++ rather than roll your own object model in C, but QEMU is quite old so it’s somewhat understandable. I say that as someone who mostly writes C and generally doesn’t like using C++.
Fabrice also wrote the Tiny C compiler, so very much his language of choice ..
For those used to the language it was seen as "lighter" and easier to add OO like abstractions to your C usage than bog down in the weight and inconsistencies of (early) C++
Every language has inconsistencies, and C is not stranger to that. Much of c++’s baggage is due to C and you carry the same weight. That’s not to say that initialization isn’t broken in C++, but just like many features in many languages (off the top of my head in C - strcpy, sprintf, ctime are like hand grenades with the pin pre pulled for you) don’t use them. There’s a subset of C++17 that to me solves so many issues with C and C++ that it just makes sense to use. An example from a codebase I spend a lot of time in is
int val;
bool valueSet = getFoo(&val);
if (valueSet) {}
printf(“%d”, val); // oops
The bug can be avoided entirely with C++
if (int val; getFoo(&val)) // if you control getFoo this could be a reference which makes the null check in getFoo a compile time check
{}
printf(“%d”, val); // this doesn’t compile.
Which sounds like the same? Now you can declare a variable and it value is not directly evaluated, you also can compare it in a condition. I think both are neat features of C++, without adding complexity.
Variables like "valueSet" scream out that the language lacks a Maybe type instead. One of the worst things about C++ is that it's content to basically not bother improving on the C type system.
I would vouch that C++ has plenty of improvements of C type system, even C++ARM already provided enough improvements that I never liked plain old C, other than that year spent learning C via Turbo C 2.0, before being given access to Turbo C++ 1.0 for MS-DOS in 1993.
The problem is all the folks that insist coding in C++ as if it was C, ignored all those C++ improvements over C.
Six divided by minus one is a "Division by zero" now? Where I come from that's minus six.
Good luck to WG14 (or maybe a faction within it?) as they seem to have decided to go make their own C++ competitor now, it's a weird time to do that, but everybody needs a hobby.
This is one example. Off the top of my head std.array vs "naked" C arrays, string vs const char*, and let's not forget RAII are all features that just make me never want to work with vanilla C ever again.
C has less moving parts — it’s more difficult to define a subset of C++ that actually works across all platforms featuring a C++ compiler, not to mention of all the binary-incompatible versions of the C++ standard library that tend to exist — and C is supported on a wider variety of platforms. If you want to maximize portability, C is the way to go, and you run into much fewer problems.
Only in certain limited cases, for example, can't have static class instances or anything else that could require calling before a call from "extern C" API.
Also now you have to build enough of a C API to expose the features, extra annoying when you want the API to be fast so it better not involve extra level of indirections through marshalling (hello, KDE SMOKE)
At some point you're either dealing with limited non-C++ API, or you might find yourself doing a lot of the work twice.
"In the strict mathematical sense, C isn't a subset of C++. There are programs that are valid C but not valid C++ and even a few ways of writing code that has a different meaning in C and C++. However, C++ supports every programming technique supported by C. Every C program can be written in essentially the same way in C++ with the same run-time and space efficiency. It is not uncommon to be able to convert tens of thousands of lines of ANSI C to C-style C++ in a few hours. Thus, C++ is as much a superset of ANSI C as ANSI C is a superset of K&R C and much as ISO C++ is a superset of C++ as it existed in 1985.
Well written C tends to be legal C++ also. For example, every example in Kernighan & Ritchie: "The C Programming Language (2nd Edition)" is also a C++ program. "
> For example, every example in Kernighan & Ritchie: "The C Programming Language (2nd Edition)" is also a C++ program. "
That is rather dated, they do things like explicitly cast the void* pointer returned by malloc, but point out in the appendix that ANSI C dropped the cast requirement for pointer conversions involving void, C++ does not allow implicit void conversions to this day.
- instead of setting the same function pointers on structs over and over again, point to a shared (singleton) struct named "vtable" which keeps track of all function pointers for this "type" of structs
- create a factory function that allocates memory for the struct, initializes fields ("vtable" included), let's call it a "constructor"
- make sure all function signatures in the shared struct start with a pointer to the original struct as the first parameter, a good name for this argument would be "this"
- encode parameter types in the function name to support overloading, e.g. "func1_int_int"
- call functions in the form of "obj->vtable->func1_int_int(obj, param1, param2)"
No, that is essentially what Linux does in this article (and by the looks of it also ffmpeg).
struct file does not have a bunch of pointers to functions, it has a pointer to a struct file_operations, and that is set to a (usually / always?) const global struct defined by a filesystem.
As you can see, the function types of the pointers in that file_operations struct take a struct file pointer as the first argument. This is not a hard and fast rule in Linux, arguments even to such ops structures are normally added as required not just-in-case (in part because ABI stability is not a high priority). Also the name is not mangled like that because it would be silly. But otherwise that's what these are, a "real" vtable.
Surely this kind of thing came before C++ or the name vtable? The Unix V4 source code contains a pointers to functions (one in file name lookup code, even) (though not in a struct but passed as an argument). "Object oriented" languages and techniques must have first congealed out of existing practices with earlier languages, you would think.
It's how you do it in many C++ implementations, but IIRC it's not actually mandated in any way unless you strive for GCC's IA-64 ABI compatibility (the effective standard on Linux for C++)
C++'s vtables are also, in my experience, especially bad compared to Objective-C or COM ones (MSVC btw generates vtables specifically aligned for use with COM, IIRC). Mind you it's been 15 years since I touched that part of crazy.
It is more the other way around, COM was designed to fit with how MSVC generates vtables.
It is a simplification of OLE, and by the time the idea came up to use that approach, there were tons of OLE code since Windows 3.1.
By the way it wasn't gone away, after how Longhorn went down, it became the main API delivery mechanism on Windows, sadly improving the tooling has never been a pritority other than half-finished attempts.
I've noticed many large C projects resort to these sorts of OOP-like patterns to manage the complexity of the design and size of the code base. But I'm not aware of any one standard way of doing this in C. It seems C++ standardized a lot of these concepts, or C++ developers adopted standard patterns somehow.
Objects regardless of what shape they take, are basically an evolution of modules that can be passed around as values, instead of having a single instance of them.
That is why they are here to stay, and even all mainstream FP and LP languages offer features that provide similar capabilities, even if they get other names for the same thing.
It is like saying an artifact is useless, only because it get named differently in English and Chinese.
Interesting, in Rust those optimizations are more implicit since there's no "final" keyword when you use dynamic dispatch via trait objects. + you also got LTO.
I wonder if there are many cases where C++ will devirtualize and Rust won't.
But then again Rust devs are more likely to use static dispatch via generics if performance is critical.
In Rust objects can dynamically go in and out of having virtual dispatch. The vtable is only in the pointer to the object, so you can add or remove it. Take a non-virtual object, lend it temporarily as a dynamically dispatched object, and then go back to using it directly as a concrete type, without reallocating anything.
That's pretty powerful:
• any type can be used in dynamic contexts, e.g. implement Debug print for an int or a Point, without paying cost of a vtable for each instance.
• you don't rely on, and don't fight, devirtualization. You can decide to selectively use dynamic dispatch in some places to reduce code size, without committing to it everywhere.
• you can write your own trait with your own virtual methods for a foreign type, and the type doesn't have to opt-in to this.
> But then again Rust devs are more likely to use static dispatch via generics if performance is critical.
Put another way, in C++ the dynamic dispatch is implicit, so you might write code which (read literally) has dynamic dispatch but the optimizer will devirtualize it. However in Rust dynamic dispatch is explicit, so, you just would not write the dynamic dispatch - it's not really relevant whether an optimizer would "fix" that if you went out of your way to get it wrong. It's an idiomatic difference I'd say.
> But then again Rust devs are more likely to use static dispatch via generics if performance is critical.
I'm not sure I follow - pretty much 99% of usage of C++ in the last, like, 20 years has been around making sure that you get static dispatch with polymorphism through templates. It's exceedingly uncommon to see the `virtual` keyword unless you have, say, some DLL-based run-time plug-in system going on.
> The interface type in golang is much more powerful than Java’s similar construct because its definition is totally disconnected from the implementation and vice versa. We could even make each codec a ReadWriter and use it all around.
This paragraph completely derailed me — I’m not familiar with golang, but `interface` in Java is like `@protocol` in Objective-C — it defines an interface without an implementation for the class to implement, decoupling it entirely from the implementation. Seems to be exactly the same thing?
The difference between Go and Java is that in Go a type need not declare its adherence to an interface up front—any type that has methods of appropriate names and signatures is considered to implement the interface, even if its designers were not aware of the interface’s existence. (This involves a small bit of dynamism in the runtime; easily cached, though, as the set of methods of a given type and the set of all interfaces are both fixed by the time the program runs.) Whether that’s a good thing depends on your design sensibilities (ETA: nominal vs structural).
Manifold is a very interesting project that adds a lot of useful features to Java (operator overloading, extension classes, and a whole bunch more). I don't know if it's smart to use it in production code because you basically go from writing Java to writing Manifold, but I still think it's a fun project to experiment with.
> Manifold is a Java compiler plugin, its features include Metaprogramming, Properties, Extension Methods, Operator Overloading, Templates, a Preprocessor, and more.
Neat tool. It is like having a programmable compiler built into your language.
The funny thing about Java is that while its design is to be entirely nominally typed, the way it is implemented in the JVM is compatible with structural typing, but there are artificial limitations set to follow the intended design (though of course, if one were to disable these limitations then modeled type safety goes out of the window as Java was simply not designed to be used that way). One community which takes advantage of this fact is the Minecraft modding space, as it is the basis[1] of how modding platforms like Fabric work.
>The difference between Go and Java is that in Go a type need not declare its adherence to an interface up front.
Go can't declare adherence up front, and in my view that’s a problem. Most of the time, explicitly stating your intent is best, for both humans reading the code and tools analyzing it. That said, structural typing has its moments, like when you need type-safe bridging without extra boilerplate.
One of the main uses for interfaces in Go is defining the contact for _your_ dependencies. Rather than saying your function takes a socket, if you only ever call Write(), you can take a Writer, or define another interface that is only the set of functions you need. This is far more powerful than declaring that your type implements an interface up front. It allows for things like e.g. multiple image libraries to implement your interface without knowing it, enabling your project to use them interchangeably. And as another commenter said, you can have the compiler verify your compliance with an interface with a simple (though admittedly odd looking) declaration.
> It allows for things like e.g. multiple image libraries to implement your interface without knowing it
That virtually never happens. Seriously, what would be the odds? It’s so much more usual to purposefully implement an interface (eg a small wrapper the writer thingy that has the expected interface) than to use something that happens to fit the expected interface by pure chance.
It’s not a structural vs nominal problem but other, typescript is structural but has the implements keyword so that the interface compliance is checked at declaration, not at the point of use. You don’t have to use it and it will work just like Go, but I found that in 99% of cases it’s what I want: the whole point of me writing this class is because I need an interface implementation, might as well enforce it at this point.
I don’t agree it’s a structural VS nominal difference. Typescript is structural, but it does have the “implements” keyword.
Which makes a million times more sense to me, because realistically when do you ever have a structure that usefully implements an interface without being aware of it?? The common use-case is to implement an existing interface (in which case might as well enforce adherence to the interface at declaration point), not to plug an implementation into an unrelated functionality that happens to expect the right interface.
TypeScript doesn't require a class to use it, though, because it's structurally typed. All that "implements Foo" in this example does is make sure that you get a type error on the definition of "One" if it doesn't have the members of "Foo".
If "Two" didn't have a "name: string" member, then the error would be on the call to "test".
interface Foo {
name: string
}
class One implements Foo {
constructor(public name: string) {}
}
class Two {
constructor(public name: string) {}
}
function test(thing: Foo): void {
//...
}
test(new One('joe'));
test(new Two('jane'));
GNU C++ once had this feature; it was called Signatures. It was removed, though.
A signature declaration resembled an abstract base class. The target class did not have to inherit the signature: just have functions with matching names and types.
The user of the class could cast a pointer to an instance of the class to a pointer to a compatible signature. Code not knowing anything about the class could indirectly call all the functions through the signature pointer.
I don't see how concepts can emulate signatures to the full extent that the target object can be manipulated as if it conformed to an abstract base, without any wrapper object being required to handle it.
Without signatures, we have to use some kind of delegating shim which takes the virtual function calls, and calls the real object. It could be a smart pointer.
With signatures, we don't use smart pointers, just "pointer to signature" pointers. However, I suspect those pointers had to be fat! Because, surely, to delegate the signature function calls to the correct functions in the target object class, we need some vtable-like entity. The signatures feature must generate such a vtable-like table for every combination of signature and target class. But target object has no space reserved in it for that table pointer. The obvious solution is a two-word pointer which holds a pointer to the object, and a pointer to the signature dispatch table specific to the signature type and target object's class.
If we can use concepts to do this, with a smart pointer that ends up being two words (e.g. pointer to its own vtable, and a pointer to the target object), we have broken even in that regard.
Shouldn’t this be named phenomenal rather than structural? In both cases there is a structure assumed, but one is implicitly inferred while the other one is explicitly required.
The difference is that in Go, an interface is assumed to match if the method signatures match. In other words, the match is done on the type structure of the interface, hence the “structural” designation. Nominal typing, on the other hand, considers that interfaces tend to be associated with important semantic requirements in addition to the type signature, and that mere type-structure matching doesn’t at all guarantee a semantic match. For that reason, the semantics are implicitly bound to the declared name of the interface, and the way for an implementation to claim conformance to those semantics is to explicitly bind itself to that name.
I think you’re making a joke, but in Go you get both. You can have the compiler enforce that you implement an interface with a simple declaration. Most people do.
No intended joke in that case, but it’s nice to have the feedback it might seen as if it was.
I don’t know Go to be frank, just had a very shallow look at it once because of an interview, and apart big names behind it, it didn’t shine in any obvious way — but that’s also maybe aligned with the "boring" tech label it seems associated with (that is, in positive manner for those who praise it).
The article is about using OO techniques directly in C code. Lua is implemented in C but it's an entirely separate language. Does its implementation use OO techniques as part of its C source code? If not, then it's not really relevant.
I don't see how a distinction here is anything but semantically arbitrary.
Transitively, it most definitely uses OO techniques. Furthermore, by having such a clean C ffi (in both directions) it allows for the weaving of the Lua based OO techniques back into C code.
> for instance, Linux handles network socket, special files (like /proc/cpuinfo) or even USB devices as files. This is a powerful idea that can make easy to write or use programs for linux since we can rely in a set of well known operations from this abstraction called file.
Benno Rice gave a fantastic talk a few years ago called "What UNIX Cost Us," which he starts off by showing how to write some USB device code in macOS, Windows, and Linux. It only takes a few minutes to demonstrate how pretending that everything is a file can be a pretty poor abstraction, and result in far more confusing code, which is why everyone ends up using libusb instead of sysfs.
That's the gist of his whole talk – that doing things "the UNIX way" (which can be defined to various degrees of specificity) has been cargo culted, and that we should reexamine whether solutions that were pragmatic 50+ years ago is still the best we can do.
The specific reason I mentioned it was because his initial example was about how much more ceremony and boilerplate is needed when you need to pretend that USB interfaces are actually magic files and directories.
For one, ffmpeg is 9 years older than Go. Plus, when dealing with video files a garbage collected language probably isn't going to cut it. C++ and Obj-C also feel overkill for ffmpeg.
CoreVideo and CoreAudio are both implemented in C on Apple systems. There are higher level APIs like AVFoundation implemented in Obj-C/Swift, but the codecs themselves are written in C. Even the mid-level frameworks like AudioToolbox and VideoToolbox are written in C. I’m not as familiar with Microsoft but imagine it’s similar.
Also the article doesn’t actually mention OOP. You can use polymorphism without fully buying into OOP (like Go does).
The great thing about C is its interoperability, which is why it’s the go to language for things like codecs, device drivers, kernel modules, etc.
This is an excellent pattern in C. The Dovecot mail server has many fine examples of the style as well e.g.
defines the virtual function table for the LDAP module, and any other subsystem that looks things up via the abstract dict interface can consequently be configured to use the ldap service without concrete knowledge of it.(those interested in a deeper dive might start at https://github.com/dovecot/core/blob/main/src/lib-dict/dict-...)
So does the good old Quake 2 rendering API. The game exported a bunch of functions to the renderer via refimport_t and the renderer in return provided functions via refexport_t. The only visible symbol in a rendering DLL is GetRefAPI_t: https://github.com/id-Software/Quake-2/blob/master/client/re...
I remember being impressed by this approach, so I shamelessly copied it for my programming game: https://github.com/dividuum/infon/blob/master/renderer.h :)
I somehow suspect that the reason why Quake2 does this lies in the legacy of Quake1 written in DJGPP. DJGPP supports dynamicaly loaded libraries (although the API is technically unsupported and internal-only), but does not have any kind of dynamic linker, thus passing around pair of such structs during library initialization is the only way to make that work.
Pretty sure Half-Life does something pretty similar - all functionality between the game and engine is done via function pointer structs.
Reminds me of Apple’s CoreFoundation.
I spend a ton of time in FFmpeg, and I’m still blown away by how it uses abstractions to stay modular—especially for a project that’s been around forever and still feels so relevant. Those filtergraphs pulling off polymorphism-like tricks in C? It’s such an elegant way to manage complex pipelines. e.g.
ffmpeg -i input.wav -filter_complex " [0:a]asplit=2[a1][a2]; [a1]lowpass=f=500[a1_low]; [a2]highpass=f=500[a2_high]; [a1_low]volume=0.5[a1_low_vol]; [a2_high]volume=1.5[a2_high_vol]; [a1_low_vol][a2_high_vol]amix=inputs=2[a_mixed]; [a_mixed]aecho=0.8:0.9:1000:0.3[a_reverb] " -map "[a_reverb]" output.wav
That said, keeping those interfaces clean and consistent as the codebase grows (and ages) takes some real dedication.
Also recently joined the mailing lists and it’s been awesome to get a step closer to the pulse of the project. I recommend if you want to casually get more exposure to the breadth of the project.
https://ffmpeg.org/mailman/listinfo
how similar are the C abstractions in ffmpeg and qemu given they were started by the same person?
I haven’t worked with ffmpeg’s code, but I have worked with QEMU. QEMU has a lot of OOP (implemented in C obviously) that is supported by macros and GCC extensions. I definitely think it would have been better (and the code would be easier to work with) to use C++ rather than roll your own object model in C, but QEMU is quite old so it’s somewhat understandable. I say that as someone who mostly writes C and generally doesn’t like using C++.
What's the reason for ffmpeg to use C, also historic?
Fabrice also wrote the Tiny C compiler, so very much his language of choice ..
For those used to the language it was seen as "lighter" and easier to add OO like abstractions to your C usage than bog down in the weight and inconsistencies of (early) C++
https://bellard.org/
https://en.wikipedia.org/wiki/Fabrice_Bellard
> weight and inconsistencies of (early) C++
Since very little is ever removed from C++, all the inconsistencies in C++ are still there.
Every language has inconsistencies, and C is not stranger to that. Much of c++’s baggage is due to C and you carry the same weight. That’s not to say that initialization isn’t broken in C++, but just like many features in many languages (off the top of my head in C - strcpy, sprintf, ctime are like hand grenades with the pin pre pulled for you) don’t use them. There’s a subset of C++17 that to me solves so many issues with C and C++ that it just makes sense to use. An example from a codebase I spend a lot of time in is
The bug can be avoided entirely with C++For C users. And C++ users:
In C++ we can declare variable in the while or if statement:
https://en.cppreference.com/w/cpp/language/while
https://en.cppreference.com/w/cpp/language/if
It's value is the value of the decision. This is not possible with C [1].
Since C++17 the if condition can contain an initializer: Ctrl+F if statements with initializer
https://en.cppreference.com/w/cpp/language/if
Which sounds like the same? Now you can declare a variable and it value is not directly evaluated, you also can compare it in a condition. I think both are neat features of C++, without adding complexity.
[1] Also not possible with Java.
Declaring a variable in a loop or if statement is supported since C99: https://en.wikipedia.org/wiki/C99
Also in Java: https://www.geeksforgeeks.org/for-loop-java-important-points...
No, declaring a variable in a `for` loop is supported in C99 but you can't do if-init.
Ah, you're right (finally tried it out). But it 100% works in a loop (usually for loop)
You could write this in C, no?
Both ways of expressing this are weird, but stating that this can't be achieved with C is dishonest in my opinion.If I reformat this,
The bug is still possible, as you've introduced an extra scope that doesn't exist in the C++ version.Also, this was one example. There are plenty of other examples.
it's not that weird to explicitly limit the scope of certain variables that logically belong together.
But i agree the C++ if(init;cond) thing was new to me.
Like everything in C++, it has it's share of footguns
vs Is probably the biggest pitfall. Especially if you're used to this:Variables like "valueSet" scream out that the language lacks a Maybe type instead. One of the worst things about C++ is that it's content to basically not bother improving on the C type system.
C++ has optional, but I wanted to demonstrate that you could wrap a C API in a safer more ergonomic way.
If you rewrote it in a more modern way and changed the API
There are lots of improvements over C’s type system - std.array, span, view, hell even a _string_ classI would vouch that C++ has plenty of improvements of C type system, even C++ARM already provided enough improvements that I never liked plain old C, other than that year spent learning C via Turbo C 2.0, before being given access to Turbo C++ 1.0 for MS-DOS in 1993.
The problem is all the folks that insist coding in C++ as if it was C, ignored all those C++ improvements over C.
C++ has a maybe type. It's called std::optional.
Here is my experimental maybe type for C: https://godbolt.org/z/YxnsY7Ted
Six divided by minus one is a "Division by zero" now? Where I come from that's minus six.
Good luck to WG14 (or maybe a faction within it?) as they seem to have decided to go make their own C++ competitor now, it's a weird time to do that, but everybody needs a hobby.
This will be in C2Y and is already supported by GCC 15: https://godbolt.org/z/szb5bovxq
This is one example. Off the top of my head std.array vs "naked" C arrays, string vs const char*, and let's not forget RAII are all features that just make me never want to work with vanilla C ever again.
C has less moving parts — it’s more difficult to define a subset of C++ that actually works across all platforms featuring a C++ compiler, not to mention of all the binary-incompatible versions of the C++ standard library that tend to exist — and C is supported on a wider variety of platforms. If you want to maximize portability, C is the way to go, and you run into much fewer problems.
Much easier to link / load into other language binaries surely.
extern “C” works just fine.
Only in certain limited cases, for example, can't have static class instances or anything else that could require calling before a call from "extern C" API.
Also now you have to build enough of a C API to expose the features, extra annoying when you want the API to be fast so it better not involve extra level of indirections through marshalling (hello, KDE SMOKE)
At some point you're either dealing with limited non-C++ API, or you might find yourself doing a lot of the work twice.
Only if your entire API doesn't contain any C++.
C is also C++, at least the C89 subset.
it actually isn't
https://www.stroustrup.com/bs_faq.html#C-is-subset
Do you usually post links without reading them?
"In the strict mathematical sense, C isn't a subset of C++. There are programs that are valid C but not valid C++ and even a few ways of writing code that has a different meaning in C and C++. However, C++ supports every programming technique supported by C. Every C program can be written in essentially the same way in C++ with the same run-time and space efficiency. It is not uncommon to be able to convert tens of thousands of lines of ANSI C to C-style C++ in a few hours. Thus, C++ is as much a superset of ANSI C as ANSI C is a superset of K&R C and much as ISO C++ is a superset of C++ as it existed in 1985.
Well written C tends to be legal C++ also. For example, every example in Kernighan & Ritchie: "The C Programming Language (2nd Edition)" is also a C++ program. "
> For example, every example in Kernighan & Ritchie: "The C Programming Language (2nd Edition)" is also a C++ program. "
That is rather dated, they do things like explicitly cast the void* pointer returned by malloc, but point out in the appendix that ANSI C dropped the cast requirement for pointer conversions involving void, C++ does not allow implicit void conversions to this day.
> Well written C tends to be legal C++ also
The "well written" remark is relevant.
Many style guides will consider implicit void conversions not well written C.
Naturally we are now on C23, and almost every C developer considers language extensions as being C, so whatever.
> The "well written" remark is relevant.
So using casts that hid implicit int declarations for years is "well written"?
> Many style guides will consider implicit void conversions not well written C.
I could not find a "Many" guide, Linux Kernel and ffmpeg seem to advocate against pointless casts.
> Naturally we are now on C23,
So irrelevant to the creation time of ffmpeg and only applicable to intentionally non portable libraries.
Well written idiomatic C is certainly not valid C++.
Depends on the beholder, however it hardly matters on the days of C23, as mentioned.
I don't see your point. The thing you quote explicitly says C isn't a subset of C++.
> Well written C tends to be legal C++ also
and python2 can be written to be compatible with python3, but neither is a subset of the other
Do you usually post links without reading them?
all the time -- I call it "crowd-sourcing intelligence" :-)
QEMU's abstractions were added when Fabrice was almost completely inactive already (starting in 2008 for some command line support).
For the record, this design pattern is called a virtual method table, or vtable.
I'm surprised that this article never mentioned the term.
C++ programmers will know this pattern from the `virtual` keyword.
You can take it a step further:
- instead of setting the same function pointers on structs over and over again, point to a shared (singleton) struct named "vtable" which keeps track of all function pointers for this "type" of structs
- create a factory function that allocates memory for the struct, initializes fields ("vtable" included), let's call it a "constructor"
- make sure all function signatures in the shared struct start with a pointer to the original struct as the first parameter, a good name for this argument would be "this"
- encode parameter types in the function name to support overloading, e.g. "func1_int_int"
- call functions in the form of "obj->vtable->func1_int_int(obj, param1, param2)"
This is pretty much how you do COM[1] in C[2].
[1]: https://learn.microsoft.com/en-us/windows/win32/com/com-tech...
[2]: https://www.codeproject.com/Articles/13601/COM-in-plain-C
> You can take it a step further:
No, that is essentially what Linux does in this article (and by the looks of it also ffmpeg).
struct file does not have a bunch of pointers to functions, it has a pointer to a struct file_operations, and that is set to a (usually / always?) const global struct defined by a filesystem.
As you can see, the function types of the pointers in that file_operations struct take a struct file pointer as the first argument. This is not a hard and fast rule in Linux, arguments even to such ops structures are normally added as required not just-in-case (in part because ABI stability is not a high priority). Also the name is not mangled like that because it would be silly. But otherwise that's what these are, a "real" vtable.
Surely this kind of thing came before C++ or the name vtable? The Unix V4 source code contains a pointers to functions (one in file name lookup code, even) (though not in a struct but passed as an argument). "Object oriented" languages and techniques must have first congealed out of existing practices with earlier languages, you would think.
The proto-C++ transpiler used C with this and similar techniques behind the scenes: https://en.wikipedia.org/wiki/Cfront
Is this satire? That’s almost exactly the C++ way.
It's how you do it in many C++ implementations, but IIRC it's not actually mandated in any way unless you strive for GCC's IA-64 ABI compatibility (the effective standard on Linux for C++)
C++'s vtables are also, in my experience, especially bad compared to Objective-C or COM ones (MSVC btw generates vtables specifically aligned for use with COM, IIRC). Mind you it's been 15 years since I touched that part of crazy.
It is more the other way around, COM was designed to fit with how MSVC generates vtables.
It is a simplification of OLE, and by the time the idea came up to use that approach, there were tons of OLE code since Windows 3.1.
By the way it wasn't gone away, after how Longhorn went down, it became the main API delivery mechanism on Windows, sadly improving the tooling has never been a pritority other than half-finished attempts.
Some say C++ is satire
It’s not satire, it’s how you do full OO in plain C.
It's overloaded - it is satire, but also, it isn't.
Exactly. Thank you for your time, I will and won't be here all week!
I've noticed many large C projects resort to these sorts of OOP-like patterns to manage the complexity of the design and size of the code base. But I'm not aware of any one standard way of doing this in C. It seems C++ standardized a lot of these concepts, or C++ developers adopted standard patterns somehow.
> I've noticed many large C projects resort to these sorts of OOP-like patterns to manage the complexity of the design and size of the code base
The Power of Interoperability: Why Objects Are Inevitable
https://www.cs.cmu.edu/~aldrich/papers/objects-essay.pdf
Objects regardless of what shape they take, are basically an evolution of modules that can be passed around as values, instead of having a single instance of them.
That is why they are here to stay, and even all mainstream FP and LP languages offer features that provide similar capabilities, even if they get other names for the same thing.
It is like saying an artifact is useless, only because it get named differently in English and Chinese.
and C++ also supports optimizing them, especially when you use `final` keyword and LTO which is able to devirtualize at the scale of a whole program.
Interesting, in Rust those optimizations are more implicit since there's no "final" keyword when you use dynamic dispatch via trait objects. + you also got LTO.
I wonder if there are many cases where C++ will devirtualize and Rust won't.
But then again Rust devs are more likely to use static dispatch via generics if performance is critical.
In Rust objects can dynamically go in and out of having virtual dispatch. The vtable is only in the pointer to the object, so you can add or remove it. Take a non-virtual object, lend it temporarily as a dynamically dispatched object, and then go back to using it directly as a concrete type, without reallocating anything.
That's pretty powerful:
• any type can be used in dynamic contexts, e.g. implement Debug print for an int or a Point, without paying cost of a vtable for each instance.
• you don't rely on, and don't fight, devirtualization. You can decide to selectively use dynamic dispatch in some places to reduce code size, without committing to it everywhere.
• you can write your own trait with your own virtual methods for a foreign type, and the type doesn't have to opt-in to this.
> But then again Rust devs are more likely to use static dispatch via generics if performance is critical.
Put another way, in C++ the dynamic dispatch is implicit, so you might write code which (read literally) has dynamic dispatch but the optimizer will devirtualize it. However in Rust dynamic dispatch is explicit, so, you just would not write the dynamic dispatch - it's not really relevant whether an optimizer would "fix" that if you went out of your way to get it wrong. It's an idiomatic difference I'd say.
> But then again Rust devs are more likely to use static dispatch via generics if performance is critical.
I'm not sure I follow - pretty much 99% of usage of C++ in the last, like, 20 years has been around making sure that you get static dispatch with polymorphism through templates. It's exceedingly uncommon to see the `virtual` keyword unless you have, say, some DLL-based run-time plug-in system going on.
yeah true, I'm really liking pornel's reply though on rust objects going between static and dynamic dispatch.
I learned it from a textbook. I think it was an earlier printing of https://docs.freebsd.org/en/books/design-44bsd/
> The interface type in golang is much more powerful than Java’s similar construct because its definition is totally disconnected from the implementation and vice versa. We could even make each codec a ReadWriter and use it all around.
This paragraph completely derailed me — I’m not familiar with golang, but `interface` in Java is like `@protocol` in Objective-C — it defines an interface without an implementation for the class to implement, decoupling it entirely from the implementation. Seems to be exactly the same thing?
The difference between Go and Java is that in Go a type need not declare its adherence to an interface up front—any type that has methods of appropriate names and signatures is considered to implement the interface, even if its designers were not aware of the interface’s existence. (This involves a small bit of dynamism in the runtime; easily cached, though, as the set of methods of a given type and the set of all interfaces are both fixed by the time the program runs.) Whether that’s a good thing depends on your design sensibilities (ETA: nominal vs structural).
For those wishing Java had a similar feature, there's Manifold: https://github.com/manifold-systems/manifold/tree/master/man...
Manifold is a very interesting project that adds a lot of useful features to Java (operator overloading, extension classes, and a whole bunch more). I don't know if it's smart to use it in production code because you basically go from writing Java to writing Manifold, but I still think it's a fun project to experiment with.
http://manifold.systems/
> Manifold is a Java compiler plugin, its features include Metaprogramming, Properties, Extension Methods, Operator Overloading, Templates, a Preprocessor, and more.
Neat tool. It is like having a programmable compiler built into your language.
The funny thing about Java is that while its design is to be entirely nominally typed, the way it is implemented in the JVM is compatible with structural typing, but there are artificial limitations set to follow the intended design (though of course, if one were to disable these limitations then modeled type safety goes out of the window as Java was simply not designed to be used that way). One community which takes advantage of this fact is the Minecraft modding space, as it is the basis[1] of how modding platforms like Fabric work.
1: https://github.com/SpongePowered/Mixin/wiki/Introduction-to-...
>The difference between Go and Java is that in Go a type need not declare its adherence to an interface up front.
Go can't declare adherence up front, and in my view that’s a problem. Most of the time, explicitly stating your intent is best, for both humans reading the code and tools analyzing it. That said, structural typing has its moments, like when you need type-safe bridging without extra boilerplate.
You can assert that your type implements an interface at compile time, though, e.g.
One of the main uses for interfaces in Go is defining the contact for _your_ dependencies. Rather than saying your function takes a socket, if you only ever call Write(), you can take a Writer, or define another interface that is only the set of functions you need. This is far more powerful than declaring that your type implements an interface up front. It allows for things like e.g. multiple image libraries to implement your interface without knowing it, enabling your project to use them interchangeably. And as another commenter said, you can have the compiler verify your compliance with an interface with a simple (though admittedly odd looking) declaration.
> It allows for things like e.g. multiple image libraries to implement your interface without knowing it
That virtually never happens. Seriously, what would be the odds? It’s so much more usual to purposefully implement an interface (eg a small wrapper the writer thingy that has the expected interface) than to use something that happens to fit the expected interface by pure chance.
It’s not a structural vs nominal problem but other, typescript is structural but has the implements keyword so that the interface compliance is checked at declaration, not at the point of use. You don’t have to use it and it will work just like Go, but I found that in 99% of cases it’s what I want: the whole point of me writing this class is because I need an interface implementation, might as well enforce it at this point.
I don’t agree it’s a structural VS nominal difference. Typescript is structural, but it does have the “implements” keyword.
Which makes a million times more sense to me, because realistically when do you ever have a structure that usefully implements an interface without being aware of it?? The common use-case is to implement an existing interface (in which case might as well enforce adherence to the interface at declaration point), not to plug an implementation into an unrelated functionality that happens to expect the right interface.
TypeScript doesn't require a class to use it, though, because it's structurally typed. All that "implements Foo" in this example does is make sure that you get a type error on the definition of "One" if it doesn't have the members of "Foo".
If "Two" didn't have a "name: string" member, then the error would be on the call to "test".
GNU C++ once had this feature; it was called Signatures. It was removed, though.
A signature declaration resembled an abstract base class. The target class did not have to inherit the signature: just have functions with matching names and types.
The user of the class could cast a pointer to an instance of the class to a pointer to a compatible signature. Code not knowing anything about the class could indirectly call all the functions through the signature pointer.
Nowadays you can do that with concepts.
I don't see how concepts can emulate signatures to the full extent that the target object can be manipulated as if it conformed to an abstract base, without any wrapper object being required to handle it.
Without signatures, we have to use some kind of delegating shim which takes the virtual function calls, and calls the real object. It could be a smart pointer.
With signatures, we don't use smart pointers, just "pointer to signature" pointers. However, I suspect those pointers had to be fat! Because, surely, to delegate the signature function calls to the correct functions in the target object class, we need some vtable-like entity. The signatures feature must generate such a vtable-like table for every combination of signature and target class. But target object has no space reserved in it for that table pointer. The obvious solution is a two-word pointer which holds a pointer to the object, and a pointer to the signature dispatch table specific to the signature type and target object's class.
If we can use concepts to do this, with a smart pointer that ends up being two words (e.g. pointer to its own vtable, and a pointer to the target object), we have broken even in that regard.
So golang supports 'duck typing'?
I think in a static context, it's generally referred to as structural typing, but yeah.
interfaces in Go are structural. Interfaces in Java are nominal and require immediate declaration of intent to implement at type definition.
Shouldn’t this be named phenomenal rather than structural? In both cases there is a structure assumed, but one is implicitly inferred while the other one is explicitly required.
The difference is that in Go, an interface is assumed to match if the method signatures match. In other words, the match is done on the type structure of the interface, hence the “structural” designation. Nominal typing, on the other hand, considers that interfaces tend to be associated with important semantic requirements in addition to the type signature, and that mere type-structure matching doesn’t at all guarantee a semantic match. For that reason, the semantics are implicitly bound to the declared name of the interface, and the way for an implementation to claim conformance to those semantics is to explicitly bind itself to that name.
I think you’re making a joke, but in Go you get both. You can have the compiler enforce that you implement an interface with a simple declaration. Most people do.
No intended joke in that case, but it’s nice to have the feedback it might seen as if it was.
I don’t know Go to be frank, just had a very shallow look at it once because of an interview, and apart big names behind it, it didn’t shine in any obvious way — but that’s also maybe aligned with the "boring" tech label it seems associated with (that is, in positive manner for those who praise it).
I'd say at 20kloc of C, https://www.lua.org/ gets you as far up the Object Oriented tower as you want.
The article is about using OO techniques directly in C code. Lua is implemented in C but it's an entirely separate language. Does its implementation use OO techniques as part of its C source code? If not, then it's not really relevant.
I don't see how a distinction here is anything but semantically arbitrary.
Transitively, it most definitely uses OO techniques. Furthermore, by having such a clean C ffi (in both directions) it allows for the weaving of the Lua based OO techniques back into C code.
No discussion about polymorphism in C is complete without mentioning this macro:
https://stackoverflow.com/questions/15832301/understanding-c...
> for instance, Linux handles network socket, special files (like /proc/cpuinfo) or even USB devices as files. This is a powerful idea that can make easy to write or use programs for linux since we can rely in a set of well known operations from this abstraction called file.
Benno Rice gave a fantastic talk a few years ago called "What UNIX Cost Us," which he starts off by showing how to write some USB device code in macOS, Windows, and Linux. It only takes a few minutes to demonstrate how pretending that everything is a file can be a pretty poor abstraction, and result in far more confusing code, which is why everyone ends up using libusb instead of sysfs.
https://www.youtube.com/watch?v=9-IWMbJXoLM#t=134s
The same is visible in having to parse a bunch of Linux's more complex of the /proc entries, vs. simply using syscalls in (say) FreeBSD.
"Everything is a file" is not a bad abstraction for some things. It feels like Linux went the route of a golden hammer here.
That's the gist of his whole talk – that doing things "the UNIX way" (which can be defined to various degrees of specificity) has been cargo culted, and that we should reexamine whether solutions that were pragmatic 50+ years ago is still the best we can do.
The specific reason I mentioned it was because his initial example was about how much more ceremony and boilerplate is needed when you need to pretend that USB interfaces are actually magic files and directories.
[dead]
My recollection (which could be rusty, it has been >30 years) is that the Motif API, coded in C, implemented a kind of polymorphism.
What is with the incorrect function declarations? I see:
int (func)().
Maybe you meant: int * (*func)(void)?
Don't mean to be pedantic. Just wanted to point it out so you can fix it.
The language is called Go.
Other than that, yeah doing by hand what C++ and Objective-C do automatically.
For one, ffmpeg is 9 years older than Go. Plus, when dealing with video files a garbage collected language probably isn't going to cut it. C++ and Obj-C also feel overkill for ffmpeg.
Apparently someone has not read the article, otherwise you would have had understood my point about Go.
Secondly, Apple and Microsoft, do just fine with Objective-C and C++ for their video codecs, without having to manually implement OOP in C.
CoreVideo and CoreAudio are both implemented in C on Apple systems. There are higher level APIs like AVFoundation implemented in Obj-C/Swift, but the codecs themselves are written in C. Even the mid-level frameworks like AudioToolbox and VideoToolbox are written in C. I’m not as familiar with Microsoft but imagine it’s similar.
Also the article doesn’t actually mention OOP. You can use polymorphism without fully buying into OOP (like Go does).
The great thing about C is its interoperability, which is why it’s the go to language for things like codecs, device drivers, kernel modules, etc.
Does ffmpeg support SVE?
[dead]
Who would have thought that OOP could be useful!
This is not oop but polymorphism that is useful. And various forms of polymorphism are used in all kinds of programming paradigms.
Also, this is a nice way to get the damn banana without getting lost in the jungle.
polymorphism used that way is 100% the most traditional OOP possible ever
Yes, a similar approach to polymorhpism is supertypical for popular OOP-focused languages, along with other concepts.
isnt polymorphism inherently a subset of oop?
No, all popular functional languages have one flavour of polymorphism or the other.
Or, in math speak, Oop means polymorphism but polymorphism doesn't mean oop.