Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

> Having to pass the object explicitly every time feels clunky, especially compared to C++ where this is implicit.

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.



Agreed, one of the biggest design mistakes in the OOP syntax of C++ (and Java, for that matter) was not making `this` mandatory when referring to instance members.


Mandatory this can also be a major hit in readability. What if you have a class that implements the abc-formula. You get

  (- 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.


C++ and Java went for the "objects as static closures" route, where it doesn't make any sense to have a `this`. Or, they made them superficially look like static closures, which in hindsight was probably not the best idea. Anyway, Java lets you use explicit `this`, I don't recall whether C++ makes it into a footgun or not.


Both languages let you use explicit `this` but don’t mandate it. The “static closure” approach is great. I don’t like having to explicitly pass `this` as a parameter to every method call as in the OP (or worse, the confusing Python approach of forcing `self` to be explicitly written in every non-static method signature but having it be implicitly passed during method calls).

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
   }


> int x = bar + 1; // should be illegal: it can be hard to distinguish if `bar` is a local variable versus an instance member

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.


The guy was referring to the explicit case where bar is a member variable. The cases where it is in the local scope under scoping rules or the global scope are not really an issue, since you can check the function definition to find if it is local. In the case that it is not in the function definition, then it is in the global scope in C. If implicit this were not done in C++, that would also be the case in C++, provided you do the sane thing and use the std namespace for everything. Just thinking about the headaches namespaces could cause when looking for such definitions with cscope gives me yet another reason to stay away from C++ whenever possible.


this in C++ is just a regular pointer, it has no special footguns, just the typical ones you have with pointers in general


that's not really true - unlike a regular pointer, `this` is not allowed to be null, thus removing `if(this == nullptr)` is always a valid optimization to do.


It absolutely is allowed to be null:

    #include <iostream>
    struct Foo {
      void bar() {
        std::cout << this << std::endl;
      }
    };
    
    int main() {
      Foo *p = nullptr;
      p->bar();
    }
will print 0


This is undefined behavior in my understanding, it just happens to work until it doesn't.

I 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.


Just to cite what the others have told you: https://godbolt.org/z/bWfaYrqoY

    /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


    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".


That code is outside the set of valid c++ programs


Only in so much that this being null is UB, but in the real world it very much can be null and programs will sometimes crash deep inside methods called on nullptr.


Supporting an explicit this is required to be able to access the member variable when a local variable hides it under scoping rules. As another person replied, C++ does indeed have an implicit this.


You must be joking.

That would be a complete redability disaster... at least for C++. Java peeps probably won't even flinch ;)


I think the author is talking about this:

  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).


Nothing a little macro magic couldn't fix..

  #define CALL(object, function, ...) (object->ops->function(object, __VA_ARGS__))


You can also just replicate the vtable of sorts in C that keeps track of things when new objects are created.


You can just use C++ instead of reinventing a worse version of C++98!


It would be an improvement on C++ since you would avoid the horrendous error messages from ridiculously long types, slow compile times, exception handling nightmares, bloat from RTTI and constant worry that operators might not mean what you expect. That is before even mentioning entirely new classes of problems like the diamond problem. Less is more.

That said, similar macro magic is used in C generic data structures and it works very well.


What guarantee do you have that ->ops is a vtable? It could contain function pointers that don’t take an implicit this argument like struct file_operations in Linux does. It could also contain variables that are non-pointers. Neither is allowed in a vtable, but both are fine to do in C.


As it isn't the compiler that creates the vtable, you can also have the equivalent of this as the last parameter or where you want it to be.

> 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? :-)


You can take the API for a linked list and implement a balanced binary search tree behind it, but continuing to call it a linked list after doing that is wrong. Similarly, you can do pointer indirections the same way a vtable would do them, but if the things you get are not the equivalent of member function pointers, it is not a vtable.


> In computer programming, a virtual method table (VMT), virtual function table, virtual call table, dispatch table, vtable, or vftable is a mechanism used in a programming language to support dynamic dispatch (or run-time method binding). (Wikipedia)

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.


Read the definition again. The programming language here is not the one using this. It is the programmer using it.


Which means that it's not the language doing OOP, but the programmer.


The definition you quoted requires the language to be the one doing it for it to be a vtable. If the programmer is doing it, then it is not a vtable by that definition.


Ok, we can name it a vtable if it's done by the language and a wtable if it is done by the programmer. I don't care about this distinction, the mechanism is the same, the effects are the same, the implemented theory (OOP) is the same. Heck event the emitted code is the same. I guess the vtable implementations in a C++ compiler are now called wtables.


It is not the same theory, since it is an ADT, not anything object oriented. Different theories can and do overlap. The emitted code is also not the same, since there is no implicit this pointer. You are the one who posted a definition you found and then claimed that it agreed with you, when it did not and now are reneging on your use of it. You do not understand this topic as well as you think you do and this is a silly hill to die on.


> You are the one who posted a definition you found and then claimed that it agreed with you

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.


The point of a vtable is to implement virtual member functions that support inheritance and polymorphism without changing the base class implementations no matter what the child classes do. If you omit the this pointer, you cannot do inheritance that uses the this pointer in an override without changing the implementation of the parent to add the this pointer back, which would be like a tail wagging a dog. Thus, it is not a vtable. It is something similar, but different.


> The point of a vtable is to implement virtual member functions that support inheritance and polymorphism without changing the base class implementations no matter what the child classes do

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.


Yes I know. From the caller that might seem to be redundant, my argument was about the callee's side. Also it is not truely redundant, as you can write:

    object1->op->start(object2)
    superclass->op->start(object)


I think both of these invocations are invalid. Using object1's vtable methods on object2, obviously, but in the latter case: the vtable method should just point at the superclass impl, if not overridden. And if overridden and the child impl needs to call the superclass, it can just do so without dispatching through some vtable.


When you think of vtables as unique or owned by an object, then these example seem weird to you. When you think of them as orthogonal to your types/objects, these examples can be useful.

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.


I think it is wrong/weird for objects of the same type to have different vtables, yes. I would call those different types.

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 most languages, this is not possible, because they abstract over the implementation of classes. In C it is so you can be more creative. You can for example use it instead of a flag for behaviour. Why branch on a variable and then call separate methods, when you can simply assign the wanted implementation directly. If you want to know, which implementation/mode is used, comparing function pointers and scalar variables amounts to the same. It is also an easy way to get a unique number. When all implementations of that can operate on the same type, they are interchangeable.

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.


"Orthogonal" vtables are essentially traits/typeclasses.


What I really like about C is that it supports these sophisticated concepts without having explicit support for them. It just naturally emerges from the core concepts. This is what makes it feel like it just doesn't restrict the programmer much.

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.


PS, as a can't edit it anymore: To those who don't know, I was talking about CPL -> BCPL -> B -> New B -> K&R C -> C99.


You are wrong about those invocations being invalid. Such patterns happen in filesystem code fairly often. The best example off the top of my head is:

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.


I don't agree that your example pattern matches to the example I'm complaining about. vfs_rename() is using old_dir's vtable on old_dir. The vtable matches the object.


It is not a vtable. It is a structure of function pointers called struct inode_operations. It is reused for all inodes in that filesystem. If you get it from one callback, you can safely use it on another struct inode on the same filesystem without a problem because of that, because nobody uses this like a vtable to implement an inheritance hierarchy. There are even functions in struct inode_operations that don’t even require the inode structure to be passed, such as ->readlink, which is most unlike a vtable since static member functions are never in vtables:

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.


Again, this just isn't responsive to my comments, which discuss the article. The article's author is clearly talking about OOP. You've invented some alternative article and are arguing about that instead; I'm not interested.


You were not discussing the article in your previous reply. That said, I have repeated explained why both you and the author article are wrong to describe that structure of function pointers as a vtable by giving examples of it containing things that are not allowed in vtables (if say a C++ compiler did this to its vtables, it could cause the wrong function to be called when trying to call a static member function). You ignore that.


The term "vtable" is not exclusive to C++. Trait function dictionaries in Rust are called vtables and behave as described here, there's not necessarily any 'this' or 'self' object.


The point of the vtable is to allow dynamic dispatch based on the actual type of an object. When you have a function that does not need a this pointer, it no longer depends on the type of the object and putting it in there anyway could cause you to execute a variant depending on the type of the object, which seems like a buggy undesirable behavior.


It can still depend on the type, the answer just doesn't need information from the instance.

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.


Let’s say I have ClassA::Foobar() and ClassB::Foobar(), and ClassB inherits from ClassA. Now let’s say I want to use ClassA::Foobar(), and I access it from the object because this pointer is in the vtable and the object is of type ClassB. Now ClassB::Foobar() executed, which is wrong. This is why static functions are not put into vtables. What Linux is doing is not a vtable, even if it shares similarities. Perhaps you would understand this if I said all thumbs are fingers, but not all fingers are thumbs.


That's a very good example.

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


You don’t implement calls to static member functions by putting them into a vtable, which is one of many reasons why this is not a vtable. The proper way to implement static functions is through static dispatch.


> You don’t implement calls to static member functions by putting them into a vtable

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.


If you leave out the this pointer, it is the equivalent of putting a function pointer to a global function (or a static member function) into the structure. This is not how OOP works.

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.


> If you leave out the this pointer, it is the equivalent of putting a function pointer to a global function (or a static member function) into the structure.

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.


> 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.


In code I write, I can know what variables mean. The feature loses its point, when it's not mandatory. Also being explicit allows you to be more expressive with variable name and ordering.


The implicit this sounds to me like magic. Magic!

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.


I don't quite agree, especially because the implicit this not only saves you from explicitly typing it, but also because by having actual methods you don't need to add the struct suffix to every function.

    mystruct_dosmth(s);
    mystruct_dosmthelse(s);
vs

    s->dosmth();
    s->dosmthelse();


My problem with implicit this is more, that you can access member variables, without it being explicit, i.e. about the callee, not about the caller.

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.


This is not the same, you introduced dynamic function resolution (i.e.a function pointer tied to a specific instance), we are talking about static function resolution (purely based on the declared type).


True, if you don't trust the compiler to optimize that, then you must live with the C naming.


You can also get clever with macros.


...and C++ added explicit this parameters (deducing this) in C++23.


“this” is a reserved keyword in C++, so you do not need to worry about it being a global variable.

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.


It’s not about ‘this’ being a global, it’s if you see ‘i++’ in code it’s not obvious if ‘i’ is a member or not without having to check context.


If you see "i++" in code and you don't have any context about what "i" is, then what difference does it make if "i" is a member variable, global variable, parameter, etc etc...

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.


Not allowing a variable to implicitly refer to a member variable makes it much easier to find. If it is not declared in the function and there is no implicit dereferencing of a this pointer, the variable is global. If the variable name is commonly used and it is a member variable, it is a nightmare to hunt for the correct declaration in the codebase with cscope.


Good point. I had misunderstood the previous comment as suggesting that this be passed to the member function as an explicit argument, rather than requiring dereferences of this be explicit. The latter makes far more sense and I agree it makes reasoning about things much easier.




Consider applying for YC's Summer 2026 batch! Applications are open till May 4

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: