I personally don't like implicit this. You are very much passing a this instance around, as opposed to a class method. Also explicit this eliminates the problem, that you don't know if the variable is an instance variable or a global/from somewhere else.
People typically use some kind of naming convention for their member variables, e.g. mFoo, m_Foo, m_foo, foo_, etc., so that's not an issue. I find `foo_` much more concise than `this->foo`. Also note that you can use explicity this in C++ if you really want to.
object->ops->start(object)
Where not only is it explicit, but you need to specify the object twice (once to resolve the Vtable, and a second time to pass the object to the stateless C method implementation). object1->op->start(object2)
superclass->op->start(object)In the first example, object1 and object2 can very much be of the same type or compatible types/subtypes/supertypes. Having vtables per object as opposed to per class to me indicates, that it IS intended to modify the behaviour of an object by changing it's vtable. Using the behaviour of another object of the same type to treat the second object, seams valid to me.
In the second case, it's not about the child implementation dispatching to the superclass, it's about some external code wanting to treat it as an object of the supertype. It's what in other languages needs an upcast. And the supertype might also have dynamic behaviour, otherwise you of course wouldn't use a vtable.
Upcasting is fine, but generally speaking the expected behavior of invoking a superclass method on an object that is actually a subclass is that the subclass method implementation is used (in C++, this would be a virtual/override type method, as opposed to a static method). Invoking a superclass-specific method impl on a subclass object is kind of weird.
In C you can also change the "class" of an instance as needed, without special syntax. Maybe you need to already call a method of the new/old class, before/after actually changing the class type.
> is that the subclass method implementation is used
The entire point of invoking the superclass method is, because the subclass has a different implementation and you want to use the superclass implementation.
Maybe it's a bit due to its evolution. It started with a language that should have all features every possible, that was to complicated to be implemented at the time. Then it was dumbed down to a really simple language. And then it evolved along side a project adding the features, that are truly useful.
error = old_dir->i_op->rename(rd->new_mnt_idmap, old_dir, old_dentry, new_dir, new_dentry, flags);
https://github.com/torvalds/linux/blob/master/fs/namei.c#L51...
That is a close match for the first example, with additional arguments.
It is helpful to remember that this is not object oriented programming and not try to shoehorn this into the paradigm of object oriented programming. This is data abstraction, which has similarities (and inspired OOP), but is subtly different. Data abstraction does not automatically imply any sort of inheritance. Thus you cannot treat things as necessarily having a subclass and superclass. If you must think of it in OOP terms, imagine that your superclass is an abstract class, with no implemented members, except you can instantiate a child class that is also abstract, and you will never do any inheritance on the so called child class.
Now, it is possible to implement things in such a way where they actually do have something that resembles a subclass and a superclass. This is often done in filesystem inode structures. The filesystem will have its own specialized inode structure where the generic VFS inode structure is the first member and thus you can cast safely from the generic inode structure to the specialized one. There is no need to cast in the other direction since you can access all of the generic inode structure’s members. This trick is useful when the VFS calls us via inode operations. We know that the inode pointer is really a pointer to our specialized inode structure, so we can safely cast to it to access the specialized fields. This is essentially `superclass->op->start(object)`, which was the second example.
Data abstraction is a really powerful technique and honestly, object oriented programming rarely does anything that makes me want it over data abstraction. The only thing that I have seen object oriented programming do better in practice than data abstraction is marketing. The second example is similar to C++’s curiously recurring template pattern, which adds boilerplate and fighting with the compiler with absurdly long error messages due to absurdly verbose types to achieve a result that often at best is the same thing. On top of those headaches, all of the language complexity makes the compile times slow. Only marketing could convince someone that the C++ OOP way is better.
https://www.kernel.org/doc/html/latest/filesystems/vfs.html
As I said previously, it is helpful to remember that this is not object oriented programming and not try to shoehorn this into the paradigm of object oriented programming. Calling this a vtable is wrong.
What speed limits can this road possibly have is a question I want to ask about this specific road. Yet this can be answered by referring to the country, which is already known when you create the road. But the user that asks this question can still ask this about roads in different countries, so this question still is valid.
Different objects can have different methods. When the method to be used is known at the time of object creation it can be chosen by assigning the appropriate function pointer in the vtable. The method itself might not necessarily need a instance pointer though.
When I invoke object->Foobar() I want to invoke the appropriate method for this object, from whatever class that might be. This is exactly what's happening in the kernel here.
When I actually intend to call the method from ClassA, I would either call something like object->base->Foobar() or ClassA->Foobar(object). Note how this is the very example that you are replying to: https://news.ycombinator.com/user?id=1718627440
Yes, you claimed it is like a static member function, I don't think it can be.
> The proper way to implement static functions is through static dispatch
Yes, but we are talking about dynamic dispatch here.
To quote your earlier comment:
> which is most unlike a vtable since static member functions are never in vtables
You conclude it isn't a vtable, I conclude, it's not a static member function, because it uses dynamic dispatch.
I don't think we actually disagree on how and when to use static and dynamic dispatch.
Omitting the this pointer also breaks inheritance, since hypothetical child classes would not be able to override the definition while using the this pointer. Having to edit the parent class to be able to do that is not how OOP works.
This is not OOP nor is it intended to be.
No it is not. Unless you can show me a virtual overridden static member function.
> since hypothetical child classes would not be able to override the definition while using the this pointer
Yes! That's the entire idea here. The non-hypothetical, but indeed existing, child classes are not able to access the object. This is to prescribe potential behaviour for child implementations.
#define CALL(object, function, ...) (object->ops->function(object, __VA_ARGS__))That said, similar macro magic is used in C generic data structures and it works very well.
> It could also contain variables that are non-pointers.
The convention of it being a pure vtable is that it just doesn't.
> Neither is allowed in a vtable
Who is the vtable membership authority? :-)
When its a table of function pointers used for dynamic dispatch, to me it's a vtable. I don't care about their type signatures as long as they logically belong to the object in question.
You seem to have a different very narrow definition of vtables, so the discussion is kind of useless.
X is Y used in Z. Does that mean X is not Y when it is not used in Z? A knife is a sharp object used to cut meat. How do I call the thing to cut fish?
Yes, I see how you can parse the definition your way, I didn't thought about that before introducing it.
> since it is an ADT, not anything object oriented
Yes, they have large overlap. To my understanding, the difference is, that OOP has inheritance and an ADT can be provided by the big god object. The latter I would refrain to call OOP, although it can be argued it still is.
I think our real discussion here is, whether a function entry that doesn't take an argument of the object type precludes it being a vtable. To me it is as long as this function is supposed to be a method of a single object and not for all objects in general, i.e. a global function.
I agree.
> If you omit the this pointer, you cannot do inheritance that uses the this pointer in an override
Yes, but:
> If you omit the this pointer, you cannot do inheritance
You can absolutely do inheritance when the child implementation doesn't need a this pointer. You need the this pointer to read/write values of the instance not to know which types it is.
Let me give you an example:
class Vehicle {
virtual unsigned int get_number_of_wheels () = 0;
};
class Car: Vehicle;
class Bicycle: Vehicle;
unsigned int Car::get_number_of_wheels () { return 4; }
unsigned int Bicycle::get_number_of_wheels () { return 2; }
This is clearly OOP, there is inheritance, a base class, a vtable introduced by the compiler. C++ will still have a this pointer here for consistency, but absolutely isn't needed here.This is exactly was is going on with check_flags(). The signature is inherited from a (virtual) base class. Part of that interface contract is that the allowed values are only depended upon the file object's type and not on it's values. You choose the implementation invoked at object construction, if it would need a this pointer, it means that the values returned can change during the lifetime of the object.
The following commands manipulate the flags associated with a file descriptor. [...]
F_SETFD (int)
Set the file descriptor flags to the value specified by arg.
Why should the set of allowed flags change over the life time of a file descriptor? That is what the kernel prevents here in it's internal interface by refusing to provide a this pointer to a child implementation.That said, I like having a this pointer explicitly passed as it is in C with ADTs. The functions that do not need a this pointer never accidentally have it passed from the developer forgetting to mark the function static or not wanting to rewrite all of the function accesses to use the :: operator.
If all you see in code is a very tiny 3 character expression, you won't be able to make much of a judgement about it to begin with.
What I don’t like is being able to reference instance members without `this`, e.g.
void foo() {
int x = bar + 1; // should be illegal: it can be hard to distinguish if `bar` is a local variable versus an instance member
int y = this->bar + 1; // disambiguation is good
}If it was this->bar it could be a member, it could also be a static variable. A bar on its own could be local or it could be in any of the enclosing scopes or namespaces. Forcing "this" to be explicit doesn't make the code any clearer on its own.
#include <iostream>
struct Foo {
void bar() {
std::cout << this << std::endl;
}
};
int main() {
Foo *p = nullptr;
p->bar();
}
will print 0I wouldn't be surprised if any null check against this would be erased by the optimizer for example as the parent comment mentioned. Sanitizers might check for null this too.
Foo *p = nullptr;
p->bar();
That's undefined behavior. It's "allowed" in the sense of "yes it's possible to write, compile and run that code", but the language makes no guarantees about what the results will be. So maybe not the most useful definition of "allowed". /app/example.cpp:10:6: runtime error: member call on null pointer of type 'Foo'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /app/example.cpp:10:6
/app/example.cpp:3:8: runtime error: member call on null pointer of type 'Foo *'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /app/example.cpp:3:8 (- this->b + sqrt(this->b * this->b - 4 this->a * this->c))/(2 * this->a)
and (- this->b - sqrt(this->b * this->b - 4 this->a * this->c))/(2 * this->a)
This is a readability problem for any class that is used to do computations.That would be a complete redability disaster... at least for C++. Java peeps probably won't even flinch ;)
Ask how do I do this, well see it's magic. It just happens.
Something went wrong? That's also magic.
After 40 years I hate magic.
mystruct_dosmth(s);
mystruct_dosmthelse(s);
vs s->dosmth();
s->dosmthelse();For the function naming, nothing stops you from doing the same in C:
static dosmth (struct * s);
s->dosmth = dosmth;
That doesn't stop you from mentioning s twice. While it is redundant in the common case, it isn't in every case like I wrote elsewhere. Also this is easily fixable as written several times here, by a macro, or by using the type directly.I wrote about this concept[1] for my own understanding as well -- just tracing the an instance of the pattern through the tmux code.
[0] https://raw.githubusercontent.com/tmux/tmux/1536b7e206e51488... [1] https://blog.drnll.com/tmux-obj-oriented-commands
It would be nice though, if syntax like the following would be supported:
struct A
{
int a;
};
struct B
{
int b;
struct A a;
};
void foo (struct A * a)
{
struct B * b;
&b->a = pa;
}
struct B b;
foo (&b.a);Of course casting to a subclass isn't guaranteed to succeed always, but for example when you have actually declared it as the subclass elsewhere it's fine without checking for isinstance.
There is a saying “If Your Only Tool Is a Hammer Then Every Problem Looks Like a Nail”. That is precisely what is happening here with the insistence to call what appears to be all structures of function pointers vtables. A vtable is something that follows a fairly well defined pattern for implementing inheritance. Not all things containing function pointers are vtables.
That's nice, but the entire point is, that the caller doesn't know the type of the object, it only has a supertype. That's why you need dynamic dispatch here. Of course you can implement dynamic dispatch without function pointers, but it is done with function pointers here. If you don't want to name dynamic dispatch implemented with function pointers a vtable, OK, that's fine, but that's the definition I am familiar with.
Read my previous comment for the bug that would happen if what us being used in Linux were actually used for dynamic dispatch when implementing inheritance, and it should be clear this is something similar, but different.
> Read my previous comment for the bug
No this is not a bug. The entire point of inheritance or dynamic dispatch IS that you call a function from "a different class" aka the subclass the object is an instance of. This is not a bug, this is the entire point of implementing it with vtables.
Yes. My point is, that it can't be a static member function, because it's overridden by subclasses.
Maybe you think that, because a this pointer is needed for dynamic dispatch? A this pointer exists there, it is just not passed to the implementation.
IMO it's much easier to write resleaks/double-frees with refcounted objects in C than it is in C++
Is a thing since C++20
> sane treatment of the void type
If you're talking about conversion rules, explicit cast from void* to T* makes more sense than implicit since it is a downcast.
> VLAs
IMO these are a mistake (and C++ templates remove a lot of the need for it). They give a false sense of security and invite stack boundary overrun as many people forget to check bounds on these. I found and reported an unauthenticated RCE DoS (crash) in a distributed DB due to VLAs; worse, one cannot always assume the minimum stack size on a system.
> a local error handling convention
Exceptions are problematic in their implementation and how they are (mis)used, but they are supposed to be orthogonal to normal control flow handling, and are not supposed to replace it. They are more-or-less recoverable panics
Yes, but you also need to specify the type in C. ((void *)p)->foo only works in New B not in C.
> IMO these are a mistake
Forgetting to check bounds does always result in these problems in C, this is not specific to VLAs. I find them useful.
> Exceptions are problematic in their implementation
Ok, but that means to me having these in the language is only a downside.
The C syntax is not really that complicated. Dynamic dispatch and virtual methods was already in the article. Here is inheritance:
struct Subclass {
struct Baseclass base;
};
That's not really that complicated. Sure, you need to encapsulate every method of the parent class, if you want to expose it. But you are also recommended to do that in other languages, and if you subclass you probably want to slightly modify behaviour anyway.As for stuff like templates: C doesn't thinks everything needs to be in the compiler. For example shadowing and hiding symbols can be done by the linker, since this is the component that handles symbol resolution across different units anyway. When you want templates, either you actually want a cheap way of runtime dynamism, then do that, or you want source code generation. Why does the compiler need to do that? For the basics there is a separate tool in the language: the Preprocessor, if you want more, you are free to choose your tool. If you want a macro language, there is e.g. M4. If you want another generator just use it. If you feel no tool really cuts it, why don't you write your code generator in C?
No need to pass in the object explicitly, etc.
Doesn't have the greatest documentation, but has a full test suite (e.g., [1][2]).
[0] https://github.com/peterpaul/co2
[1] https://github.com/peterpaul/co2/blob/master/carbon/test/pas...
[2] https://github.com/peterpaul/co2/blob/master/carbon/test/pas...
0. https://github.com/peterpaul/co2/tree/master/examples/my-obj...
> Because they like how C caters to this.
We(aka I) think we are shooting ourselves less, because C represents the algorithms more in a way how we want to express them. C's lack of syntactic sugar means dynamic dispatch is always visible. C not prescribing which function pointers you can use, means that the most fitting way can be chosen as described by the article and the LWN post, as opposed to shoehorning it into some paradigm prescribed by the language, which causes more problems done the line.
I was involved in a product with a large codebase structured like this and it was a maintainability nightmare with no upsides. Multiple attempts were made to move away from this to no avail.
Consider that the code has terrible readability due to no syntax-sugar, the compiler cannot see through the pointers to optimise anything, tooling has no clue what to do with it. On top of that, the syntax is odd and requires any newbies to effectively understand how a c++ compiler works under-the-hood to get anything out of it.
On top of those points, the dubious benefits of OOP make doing this a quick way to kill long-term maintainability of your project.
For the devs who come after you, dont try to turn C into a poor-mans C++. If you really want to, please just use C++.
To me less syntactic sugar is more readable, because you see what function call involves dynamic dispatch and which doesn't. Ideally it should also lead to dynamic dispatch being restricted to where it is needed.
I don't know where (might also have been LWN), but there was a post about it actually being more optimizable by the compiler, because dynamic code in C involves much less function pointers and the compiler can assume UB more often, because the assignments are in user code.
> requires any newbies to effectively understand how a c++ compiler
You are not supposed to reimplement a C++ compiler exactly, you are supposed to understand how OOP works and then this emerges naturally.
> dont try to turn C into a poor-mans C++
It's not poor-mans C++, when it's idiomatic C.
People like me very much choose C while having this usage in mind, because its clearer and I can sprinkle dynamism where it's needed not where the language/compiler prescribes it and because every dynamism is clear because there is not dynamic sugar, so you can't hide it.
OP doesn't use void pointers, he uses void. He writes about functions having no arguments and returning nothing for the same reason other blog posts name functions foo and bar.
> OP uses this vtable as a form of indirection to implement runtime method swapping and polymorphism
The kernel uses vtables to implement polymorphism, it doesn't store the vtable in the object to save space. If there is no polymorphism, you don't use a vtable at all, that's saving even more space.
Not true. You use a vtable even if there is no polymorphism, in cases where you want to have objects store pointers to their methods (OO interface), but don't want each instance to have pointers to all methods. I was referring to this article, which the OP links in his post https://lwn.net/Articles/444910/
You can redefine the methods at runtime. If for example I have a logger that logs on the local drive and another one that logs on a networked drive I can swap the method pointer. This is not polymorphism since the object does have to change. I can also load the function from a shared library that is unknown during compilation and use that as part of the objects methods.
ryao•5mo ago
This technique predates object oriented programming. It is called an abstract data type or data abstraction. A key difference between data abstraction and object oriented programming is that you can leave functions unimplemented in your abstract data type while OOP requires that the functions always be implemented.
The sanest way to have optional functions in object oriented programming that occurs to me would be to have an additional class for each optional function and inherit each one you implement alongside your base class via multiple inheritance. Then you would need to check at runtime whether the object is an instance of the additional class before using an optional function. With an abstract data type, you would just be do a simple NULL check to see if the function pointer is present before using it.
trws•5mo ago
ryao•5mo ago
In the Linux VFS for example, there are optimized functions for reading and writing, but if those are not implemented, a fallback to unoptimized functions is done at the call sites. Both sets are function pointers and you only need to implement one if I recall correctly.
f1shy•5mo ago
pjmlp•5mo ago
naasking•5mo ago
I think the "is/is not" question is not so clear. If you think of "is" as a whether there's a homomorphism, then it makes sense to say that it is OOP, but it can qualify as being something else too, ie. it's not an exclusionary relationaship.
ryao•5mo ago
If you think of the differences as being OOP implies contracts with the compiler and data abstraction does not (beyond a simple structure saying where the members are in memory), it becomes easier to see the two as different things.
1718627440•5mo ago
ryao•5mo ago
1718627440•5mo ago
ryao•5mo ago
You can implement OOP in C if you do vtables for inheritance hierarchies manually, among other things, but that is different than what Linux does.
1718627440•5mo ago
It's a function belonging to an object, to which is dynamically dispatched with something I would call a vtable. To me that sounds like a classic example of OOP.
Data abstraction is a core of OOP.
This pattern can be used to implement inheritance, when it isn't here that doesn't mean its not OOP.
ryao•5mo ago
1718627440•5mo ago
> Object-oriented programming (OOP) is a programming paradigm based on the object – a software entity that encapsulates data and function(s). An OOP computer program consists of objects that interact with one another. A programming language that provides OOP features is classified as an OOP language [...]
You don't disagree, that this kernel pattern is about data abstraction. You probably don't disagree, that the kernel uses functions. The kernel uses "objects" (FS implementations) that follow a defined set of functions, sometimes called "class" (vtables/wtables/however you like to call them). Therefore I conclude what the kernel does here is a prime example of OOP.
ryao•5mo ago
1718627440•5mo ago
I know ADT and OOP are different concepts, in another answer I wrote what I think the base differences are. But they are related, and in the definitions I am familiar with, ADTs are a base concept for OOP. And OOP can be an implementation for ADTs.
If you don't think the implementations I provide are enough to apply to the kernel, can maybe provide your own definition according to which we can evaluate this, because I feel like we are beating around the bush. But please not something that says this can only be happening in an OOP language, to these I would plainly disagree with, because OOP is a paradigm and not a property of a language.
teo_zero•5mo ago
Sorry, but where did you got this definition from? I've always thought OOP as a way of organizing your data and your code, sometimes supported by language-specific constructs, but not necessarily.
Can you organize your data into lists, trees, and hashmaps even if your language does not have those as native types? So you can think in a OO way even if the language has no notion of objects, methods, etc.
ryao•5mo ago
It is from experience with object oriented languages (mainly C++ and Java). Technically, you can do everything manually, but that involves shoehorning things into the OO paradigm that do not naturally fit, like the article author did when he claimed struct file_operations was a vtable when it has ->check_flags(), which would be equivalent to a static member function in C++. That is never in a vtable.
If Al Viro were trying to restrict himself to object oriented programming, he would need to remove function pointers to what are effectively the equivalent of static member functions in C++ to turn it into a proper vtable, and handle accesses to that function through the “class”, rather than the “object”.
Of course, since he is not doing object oriented programming, placing pointers to what would be virtual member functions and static member functions into the same structure is fine. There will never be a use case where you want to inherit from a filesystem implementation’s struct file_operations, so there is no need for the decoupling that object oriented programming forces.
> I've always thought OOP as a way of organizing your data and your code, sometimes supported by language-specific constructs, but not necessarily.
It certainly can be, but it is not the only way.
> Can you organize your data into lists, trees, and hashmaps even if your language does not have those as native types?
This is an odd question. First, exactly what is a native type? If you mean primitive types, then yes. Even C++ does that. If you mean standard library compound types, again, yes. The C++ STL started as a third party library at SGI before becoming part of the C++ standard. If you mean types that you can define, then probably not without a bunch of pain, as then we are going back to the dark days of manually remembering offsets as people had to do in assembly language, although it is technically possible to do in both C and C++.
What you are asking seems to be exactly what data abstraction is, which involves making an interface that separates use and implementation, allowing different data structures to be used to organize data using the same interface. As per Wikipedia:
> For example, one could define an abstract data type called lookup table which uniquely associates keys with values, and in which values may be retrieved by specifying their corresponding keys. Such a lookup table may be implemented in various ways: as a hash table, a binary search tree, or even a simple linear list of (key:value) pairs. As far as client code is concerned, the abstract properties of the type are the same in each case.
https://en.wikipedia.org/wiki/Abstraction_(computer_science)...
Getting back to doing data structures without object oriented programming, this is often done in C using a structure definition and the CPP (C PreProcessor) via intrusive data structures. Those break encapsulation, but are great for performance since they can coalesce memory allocations and reduce pointer indirections for objects indexed by multiple structures. They also are extremely beneficial for debugging, since you can see all data structures indexing the object. Here are some of the more common examples:
https://github.com/openbsd/src/blob/master/sys/sys/queue.h
https://github.com/openbsd/src/blob/master/sys/sys/tree.h
sys/queue.h is actually part of the POSIX standard, while sys/tree.h never achieved standardization. You will find a number of libraries that implement trees like libuutil on Solaris/Illumos, glib on GNU, sys/tree.h on BSD, and others. The implementations are portable to other platforms, so you can pick the one you want and use it.
As for “hash maps” or hash tables, those tend to be more purpose built in practice to fit the data from what I have seen. However, generic implementations exist:
https://stackoverflow.com/questions/6118539/why-are-there-no...
That said, anyone using hash tables at scale should pay very close attention to how their hash function distributes keys to ensure it is as close to uniformly random as possible, or you are going to have a bad time. Most other applications would be fine using binary search trees. It probably is not a good idea to use hash tables with user controlled keys from a security perspective, since then a guy named Bob can pick keys that cause collisions to slow everything down in a DoS attack. An upgrade from binary search trees that does not risk issues from hash function collisions would be B-trees.
By the way, B-trees are containers and cannot function as intrusive data structures, so you give up some convenience when debugging if you use B-Trees.
1718627440•5mo ago
You don't need classes for OOP. C++ not putting methods that logically operate on an object, but don't need a pointer to it, into the automatically created vtable, is an optimization and an implementation detail. I don't know why you think that putting this function into a vtable precludes OOP.
Wait, how does inheritance work when the method is not in the vtable?
ryao•5mo ago
As for how inheritance works when the member function is not in the vtable, that depends on what kind of member function it is. All C++ functions are given a mangled name that is stuffed into C’s infrastructure for linking symbols. For static member functions, inheritance is irrelevant since they are tied to the class. Calls to static member functions go directly to the mangled function with no indirections, just as if it had been a global function. For non-static virtual member functions, you use the vtable pointer to find it. For non-virtual member functions, the call goes straight to the function as if a global function had been called (and the this pointer is still passed, even if the function does not use it), since the compiler knows the type and thus can tell the linker to have calls there go to the function through the appropriately mangled name. It is just like calling a global function.
1718627440•5mo ago
Yes. Since we are not in C++ we can choose to get rid of this useless pointer.
> Removing it on member functions that do not use it would pose a problem if another class inherited from this class and overrode the function definition with one that did use it.
That problem has nothing to do with the this pointer specifically. When you change the method signature of an inherited method you always have this problem. This simply means, that the superclass prescribes limits to subclasses, which is why it's possible to use a subclass inplace of a superclass.
> Maybe in very special cases whole program optimization could safely remove the this pointer, but it is questionable whether any compiler author
Yes, that's why its not done in C++, but we can do it, if we handroll it.
> it would break ABI stability
It does not if it has always been like this.
> For static member functions, inheritance is irrelevant since they are tied to the class. Calls to static member functions go directly to the mangled function with no indirections
In other words, ->check_flags() can't be implemented as a static member functions in C++. It would simply have a this pointer, that it just wouldn't use, since C++ has no way to express non-static member functions, that just don't take a this pointer.
> thus can tell the linker to have calls there go to the function
In our case the linker can only resolve the call to the appropriate vtable, since the type isn't known until runtime.
ryao•5mo ago
If you were trying to implement OOP in the kernel in C and implemented a vtable, you cannot get rid of the this pointer in vtable entries since a child class might want to use it in the overrode definition. It is one of the same reasons why you cannot remove it in C++. The entire point of a vtable is to enable inheritance. If OOP really were being done, an out of tree module could make a class that inherits from this one without needing any code changes and use the this pointer, but you cannot do that if you drop the this pointer. I already explained this.
1718627440•5mo ago
You already prescribe with the chosen arguments in the superclass on which things the child implementation can depend. Why not also do this with the first argument?
ryao•5mo ago
1718627440•5mo ago
Also why do you care exactly about the order of arguments? The nature of the function doesn't change, it's entirely arbitrary and orthogonal to the paradigm the function implements. Another example is the implementation of the equality operator between objects. In languages with syntactic sugar you typically have (self, other), but if its the true equality operator then the order doesn't matter.
1718627440•5mo ago
Isn't this exactly how most (every?) OOP language implements it? You would say a C++ virtual method isn't OOP?
ryao•5mo ago
The entire point of OOP is to make contracts with the compiler that forcibly tie certain things together that are not tied together with data abstraction. Member functions are subject to inheritance and polymorphism. Member function pointers are not. Changing the type of your class will never magically change the contents of a member function pointer, but it will change the constants of a non-virtual member function. A member function will have a this pointer to refer to the class. A member function pointer does not unless you explicitly add one (named something other than this in C++).
1718627440•5mo ago
You claim when the compiler does this, it's OOP, but when I do it, it's not?
ryao•5mo ago
1718627440•5mo ago
ryao•5mo ago
1718627440•5mo ago
Of course you could implement the same in C++ and then it can't be the same as the vtable introduced by the compiler, so you would just end up with to vtables, you own and the one introduced by the compiler.
ryao•5mo ago
As I said, these are NOT vtables. The fact that you and some others keep thinking of them as vtables misleads you into thinking that this can be done using the object oriented tools of C++. It cannot without major hacks and the result would be slower, harder to read and only something that a bureaucrat could like.
1718627440•5mo ago
> As I said, these are NOT vtables
Ok, you just define vtables differently then me. To me a vtable is a table of virtual functions that are used to implement polymorphic behaviour of objects. This applies to their usage in the kernel and the article. Feel free to introduce a new term for this. If your only distinction is whether these are created by a compiler, this is just a distinction I don't care about.
ryao•5mo ago
As for the one distinction you recognize and think is invalid, that distinction is given by the definition you found. You refuse to obey the definition you yourself quoted to settle matter elsewhere in the thread.
1718627440•5mo ago
It's not the only distinction I recognize, its the distinction you think matters here and that seams to be the basis for our disagreement.
This is the term (vtable, VMT) I got told in lectures to describe this pattern, you have yet not pointed me to a different term that you would recognize to be this, so in lack of a better term I will continue to use this.
As to why I think this distinction does not matter here, is because I perceive the compiler to be a tool that generates code which is controlled by the programmer. Thus the programmer in both cases creates codes with the same paradigm, they only differ in the tools used. We generally don't name things differently depending on which tools are used in the creation, except if they are created with a different intention.
dragonwriter•5mo ago
mistrial9•5mo ago
source- I wrote a windowing framework for MacOS using this pattern and others, in C with MetroWerks at the time.
ryao•5mo ago
As for abstract data types, they originated in Lisp, which also predates object oriented programming.
pjmlp•5mo ago
"AN ALGORITHMIC THEORY OF LANGUAGE", 1962
https://apps.dtic.mil/sti/tr/pdf/AD0296998.pdf
In this paper they are known as plexes, eventually ML and CLU will show similar approaches as well.
Only much latter would Lisps evolve from plain lists and cons cells.
ryao•5mo ago
https://history-computer.com/software/simula-guide/
Thus, while I had thought Lisp had ADT concepts before the first OOL existed, now I am not sure. My remark that they originated in Lisp had been said with the intention that I was talking about the first language to have it. The idea that the concept had been described outside of an actual language is tangential to what I had intended to say, which is news to me. Thanks for the link.
hiker•5mo ago
https://dl.acm.org/doi/pdf/10.1145/366199.366256
and the paper even starts with a critique of the efficiency of Lisp's approach for representing data with cons pairs (citing McCarthy's paper from the same year).
You might also want to watch Casey's great talk on the history of OOP
https://www.youtube.com/watch?v=wo84LFzx5nI
pavlov•5mo ago
It's sad that OOP was corrupted by the excessively class-centric C++ and Java design patterns.
mettamage•5mo ago
pjmlp•5mo ago
https://en.wikipedia.org/wiki/Portable_Distributed_Objects
mettamage•5mo ago
bitwize•5mo ago
saagarjha•5mo ago
astrange•5mo ago
pjmlp•5mo ago
Also dynamic runtime dispatch Smalltalk style can never be as fast as the VMT based dispatch, or compile time dispatch via generics, even with all the optimizations in place, that objc_msgSend() has had during its lifetime.
Still, Metal is implemented in Objective-C, so there is that.
1718627440•5mo ago
ryao•5mo ago
That is my understanding of how the process generally works and what I am willing to guess happened. Prior to this, they had been making incremental changes to Objective-C.
That said, from what I have seen of the syntax of both languages, swift’s syntax is nicer and that is not something that they would have been able to get from Objective-C. They already had tried syntax reform for Objective-C once in the past and abandoned it.
astrange•5mo ago
pjmlp•5mo ago
ryao•5mo ago
Every few years, someone tells me I should learn another language, and in recent years, there just is no desire in my mind to want to learn yet another language that is merely another way of doing something that I already can do elsewhere and the only way I will is if I am forced (that is how I used Go).
That said, I do see what you are saying. C++ for example has an “support every paradigm” philosophy, so whenever someone who has learned C++ encounters a language using a paradigm that C++ assimilated, there is a huge temptation to try to view it through the lens of C++. I also can see the other side too: “C++ took me forever to learn. Why go through that again when I can use C++ as a shortcut to understand something else?”. C++ is essentially the Borg of programming languages.
1718627440•5mo ago
johnisgood•5mo ago
Based on his comment, I did not think that he is proficient in them, but that he has used them, which is fair enough, so have I, sans all the ones tied to either Apple (Swift) or Microsoft (C#).
I have some projects in Haskell just for curiosity's sake, and because what I wanted seemed like it would be nice in Haskell, and it indeed looks quite elegant to me, for this one particular project. Haskell is not a language I would use generally. OCaml is.
ryao•5mo ago
johnisgood•5mo ago
ryao•5mo ago
There are some languages in which I am extremely proficient. My best language is C, which is my favorite and I have used most features of every version of C from C89 to C11. My second best is probably either C++ or POSIX shell (although I have moments where I forget certain syntax and need to look it up, especially in POSIX shell for variants on variable substitution, e.g. ${VAR%%foobar}). I have used most features of C++98 and some from newer versions. My experiences with C++ have soured me on it, so I now try to avoid C++ whenever I can in favor of C and Python.
My first language was actually PHP 4.2.y, and I was fairly proficient in it, having spent a long time learning it while simultaneously writing my own code for a website as a teenager. However, I never once touched the portions describing objects/classes, namespaces or exceptions. Someone else at work writes Modern PHP code using Symfony and I have taken a peek at it. It looks very different from the PHP I knew because it uses the features I had avoided learning (and probably some new language features too), although I can sort of read it thanks to having learned those concepts in other languages.
I used SML/NJ and Java in college. Years after college, I modified an open source Android TV application written in Java to add some things I wanted, although honestly, beyond that I have not really touched either language. Give me an arbitrary application written in either to improve and I would have some difficulty, although I will probably be able to do it after filling the gaps in my understanding (and doing plenty of head banging if it is a large program).
I have used JavaScript for a few recent projects via electron/nodejs and I have done several small things in Python over the past several years. Each time, I only worked with the subset that I need. I am far from a master of either language that can understand arbitrary code written in them, but I am able to manage as far as my specific needs are concerned.
I could continue listing my experiences doing things in languages (like the time in college that I wrote some basic programs in FORTRAN 90 to try to learn it), but they really are not that interesting. It was often a project here or a small application there, and as I readily admitted, I only used a subset of most of the languages. For programming, a subset of commonly used bits is often all you need.
bluGill•5mo ago
smalltalk is not original OO - c++ took oo from simula which was always a different system.
moregrist•5mo ago
This introduces performance issues larger than the typical ones associated with vtable lookups. Not all domains can afford this today and even fewer in the 80s/90s when these languages were first designed.
> It's sad that OOP was corrupted by the excessively class-centric C++ and Java design patterns.
Both Smalltalk and Objective-C are class based and messages are single-receiver dispatched. So it’s not classes that you’re objecting to. It’s compile-time resolved (eg: vtable) method dispatch vs a more dynamic dispatch with messages.
Ruby, Python, and Javascript all allow for last-resort attribute/message dispatching in various ways: Ruby via `method_missing`, Python by supplying `__getattr__`, and Javascript via Proxy objects.
1718627440•5mo ago
frutiger•5mo ago
1718627440•5mo ago
We are talking about an optimization of a language implementation here. This would be very much written in a ASM or another language were this is defined.
chunkyguy•5mo ago
Don't know about other programming languages but with Objective-C due to IMP caching the performance is close to C++ vtable
https://mikeash.com/pyblog/friday-qa-2016-04-15-performance-...pjmlp•5mo ago
In Smalltalk systems that stop being an issue after JITs got introduced.
MangoToupe•5mo ago
baranul•5mo ago
Often class-based programming is confused as being the only style of OOP, superior to all other styles, or heavy-handedly pushed on others. Many programmers are perfectly fine with using objects or only specific features of OOP, without classes, if they are "allowed" to.
1718627440•5mo ago
I would rather say that OOP is a formalization of predating patterns and paradigma.
ryao•5mo ago
For full disclosure, I have never verified that leaving ->read() unimplemented when ->read_iter() is implemented is safe, but I have seen enough examples of code that I strongly suspect it is and if it is not, it is probably a bug.
1718627440•5mo ago
I think this is more of an effect of C distinguishing between allocating memory (aka object creation) and initialization, which other languages disallow for other reasons, not because there are not OOPy enough.
ryao•5mo ago
pjmlp•5mo ago
p_l•5mo ago
kazinator•5mo ago
So that is to say, if the base class has a certain function which must be implemented and must provide certain behaviors, then the derived class must implement that function and provide all those behaviors.
The POSIX functions like read and write do not have a contract which says that all implementations of them must successfully transfer data. Being unimplemented (e.g returning -1 with errno EOPNOTSUPP or whatever) is allowed in the contract.
OOP just wants a derived thing to obey the contract of the abtraction it is inheriting, so if you want certain liberties, you have to push them into the contract.
ryao•5mo ago
p_l•5mo ago
As far as OOP is concerned, lack of implementation is not an issue in instantiating something - an object will just not understand the message, possibly catastrophically.
ryao•5mo ago
p_l•5mo ago
This usually resolves to a function call because it's the easiest and most sensible way to do it.
Objective-C is more explicit about it due to Smalltalk heritage. Some languages model objects as functions (closures) that one calls with the "message" (an old trick in various FP languages is to implement a trivial object system with closures for local state).
[1] Arguably CLOS with Generic Functions can be seen as outlier, because the operation becomes centerpiece, not the object.
kazinator•5mo ago
1718627440•5mo ago
ryao•5mo ago
1718627440•5mo ago
ryao•5mo ago
1718627440•5mo ago
1718627440•5mo ago
kazinator•5mo ago
The LSP is never absolute in a practical system. Because why would you have, say, in a casee of inheritance, a Y which is an new kind of X, if all it did was substitute for X, and behave exactly the same way? Just use X and don't create Y; why create a substitute that is perfectly identical.
If there is a reason to introduce a new type Y which can be used where X is currently used, then it means they are not substitutable. The new situations need a Y and not X, and some situations don't need a Y and stay with X.
In a graphics program, an ellipse and rectangle are substitutable in that they have a draw() method and others, so that they plug into the framework.
But they are not substitutable in the user's design; where the user needs an ellipse, a rectangle won't do, and vice versa.
In that case the contract only cares about the mechanics of the program: making multiple shapes available in a uniform way, with a program organization that makes it easy to code a new shape.
The substitution principle serves the program organization, and not much more.
So with the above discussion in place, we can make an analogy: an ellipse object in a vector graphics program can easily be regarded as a version of a rectangle with unimplemented corners.
The user not onnly doesn't mind that the corners are not implemented, but doesn't want them because they wouldn't make sense.
In the same way, it doesn't make sense to have lseek() on a pipe, or accept() on TTY descriptor or write() on a file which is read only to the caller, etc.
1718627440•5mo ago
LSP is about behaviour existing in the supertype. Adding behaviour doesn't violate LSP.
> In a graphics program, an ellipse and rectangle are substitutable in that they have a draw() method and others, so that they plug into the framework.
The behaviour in question means it draws something. It can draw something different every time, and not violate LSP here.
yndoendo•5mo ago
class DefaultTask { }
class SpecialTask { }
class UsedItem {
}Is python a OOP language? Self / this / object pointer has to be passed similar to using C style object-oriented / data abstraction.
1718627440•5mo ago
zozbot234•5mo ago
1718627440•5mo ago
When a method is never overridden, it doesn't need to be in the vtable.
zozbot234•5mo ago
1718627440•5mo ago
I think composition over inheritance is only about being explicit. That's it.
maleldil•5mo ago
1718627440•5mo ago
kerblang•5mo ago
(sorry it took more than a decade for Java to catch up and Sun Microsystems originally sued Microsoft for trying to add lambdas to java way back when, and even wrote a white paper insisting that anonymous inner classes are a perfectly good substitute - stop laughing)
Shorel•5mo ago
The difference is that design patterns are a technique where you use features not implemented by the compiler or language, and all the checks have to be done by the developer, manually.
Thus, you are doing part of the work of the compiler.
In assembler, a function call is a design pattern.