The simplest possible method syntax in C

I’ve been thinking a lot about language design lately. Part of this comes from my quite successful acquisition of Go and my mostly failed attempt to learn Rust. These languages make me question premises I’ve held for a long time, and that questioning has borne some fruit.

In the remainder of this posting I will describe a simple syntax extension in C that could be used to support a trait-centered object system similar to Rust’s (or even Go’s). It is not the whole design, but it is a simple orthogonal piece that could fit with several different possible designs.

Suppose we have two structs named snark and boojum and a function that takes one of each. That is:

struct snark {
    /* stuff goes here */
}

struct boojum {
    /* other stuff goes here */
}

int conjugate(int x, struct snark *, int y, struct boojum *, int z);

struct snark s;
struct boojum b;

Then my proposal is this: under almost all circumstances, the compiler
should automatically perform the following transformations::

s.conjugate(1, 2, &b, 3) -> conjugate(1, &s, 2, &b, 3)
b.conjugate(1, &s, 2, 3) -> conjugate(1, &s, 2, &b, 3)

That is, if the compiler encounters an attempt to evaluate a structure member reference followed by an argument list, for a member that doesn’t exist in the structure (that’s the “almost”), it looks for a visible function with the right name, and then tries to apply it to transform the method-like syntax into an actual method call.

The rule will fail unless the actual arguments match all of the formal argument types of the function in the right order, except that one actual argument is missing. It will also fail unless the one formal without a corresponding actual has any type other than address of the structure for which we are synthesising the method call.

(It has to be address of, otherwise these method calls could never mutate the instance – they’d get an arg-stack copy of it instead.)

I think I got this idea by miscegenating a recent language called Julia with Rust’s method syntax and some dim memories of CLOS. It has a couple of interesting advantages:

1. No front-end modifications or new syntactic tokens are required – it can be implemented entirely as a transformation on abstract syntax after parsing.

2. Entirely upward-compatible with plain C – if you try to feed this to a compiler without the extension it will break noisily at compile time.

3. Same function can be a method of multiple structures without the requirement that you make an articial commitment to which one “really” owns it. (I find this comes up remarkably often.)

Of course, as I mentioned up front, this is not an entire object system. Conspicuous by its absence is inheritance or a trait/interface system. The final advantage of this proposal is that it would not foreclose or complicate any of those alternatives.

42 comments

  1. Shouldn’t it be:
    s.conjugate(1, 2, b, 3) -> conjugate(1, &s, 2, b, 3) //?
    What if conjugate is NOT declared?
    Also in the above, I think there is a lot more type checking or validation to figure out what “b” should be. Adding something like polymorphism or the prerequisite typing (what if “b” is (void *)?) to C is a bad idea.

    In general, I’d say the “conjugate” routine is badly designed and declared wrongly (or clunkily) for the case, so it may just be me or the example. I know how to do the thing you appear to want in actual C, but I wouldn’t have constructed the subroutine with the parameters as you did.

    As you are fond of pointing out, one patch is worth a thousand requests. Do you have one for any common C compiler to illustrate how simple it might be to implement correctly?

    1. >Shouldn’t it be: s.conjugate(1, 2, b, 3) -> conjugate(1, &s, 2, b, 3)

      Actually the b needs to become &b too. Fixed.

      >What if conjugate is NOT declared?

      Then you get the same error you would ordinarily from trying to compile an access to a nonexistent member.

      >In general, I’d say the “conjugate” routine is badly designed and declared wrongly (or clunkily) for the case, so it may just be me or the example.

      The design is more constrained than you might think. We need a method to be able to mutate the instance it was invoked on. for one thing.

      >As you are fond of pointing out, one patch is worth a thousand requests. Do you have one for any common C compiler to illustrate how simple it might be to implement correctly?

      Nope. Wanted to see if anybody shot huge holes through the concept first.

  2. I think in this example ‘conjugate’ should not be a member function regardless of language, because it always takes both a boojum and a snark. I’d expect a member of snark to take a const boojum and modify the snark, and vice versa. So this should not be implemented in a single function.

    But I’ve probably missed the point.

    Paul

    1. >But I’ve probably missed the point.

      Maybe I should have started with an example that is only a “method” of one class. The example I gave was sort of to illustrate the maximum range. The following would be more typical:

      struct person;
      
      void SetSalary(struct person *, int salary);
      
      struct person fred;
      
      .
      .
      .
      
         fred.SetSalary(54000); // becomes SetSalary(&fred, 54000)
      
  3. My first thought (having been raised on Java) was “what if conjugate is overloaded, such that both int conjugate(int x, struct snark *, int y, struct boojum *, int z); and int conjugate(int x, int y, struct boojum *, int z); exist?”, but, of course, C doesn’t support function overloading.

    But I agree with Paul, a function like conjugate() shouldn’t be a member function unless the language doesn’t support functions that aren’t member functions (which any strict superset of C has to support by definition).

  4. @esr:
    > The following would be more typical:

    That gives the syntactic sugar of OOP without the meat. The whole point is to have person.salary only be modifiable through the member functions of person, and to allow different subclasses of person to have different internal details and different implementations of SetSalary().

    For example, you might have two subclasses of person: salaried and hourly. salaried might just have the member variable “salary”, hourly might have the member variables “hours” and “HourlyRate”. Calling salaried.SetSalary() would just modify salaried.salary, whereas calling hourly. SetSalary() would divide the provided annual salary by 50 weeks per year, then by “hours”, then store the result in HourlyRate.

    1. >That gives the syntactic sugar of OOP without the meat.

      I said it was just an orthogonal part, and meant it.

      Other decisions about visibility and namespacing would have to be made for a full object system.

  5. I worry about situations like

    int conjugate(int x, struct snark *, struct boojum *, int z)

    especially if there is a class that inherits from both snark and boojum.

  6. If there were a usability win here then perhaps you could create a pre-processor that takes,

    struct person
    {
    int salary;
    void SetSalary(int salary);
    };

    struct person fred;
    fred.SetSalary(64000);

    and turns it into

    struct person
    {
    int salary;
    };
    void person_SetSalary(struct person* person, int salary);

    struct person fred;
    person_SetSalary(&fred, 65000);

    (Obviously including the random pay rise…)

    I think you need to consider decorating the ‘member’ function when it is taken out of struct, there’s no reason why multiple structs mightn’t have ‘SetSalary’ members.

    I’m not yet getting why you would want to do this, I don’t see any advantage to ‘member functions’ in an environment where there is no private/public, no inheritance, no templates, no operator overloading.

    Perhaps an approach would be to produce a ‘straitjacket’ pre-processor that barfs when given any C++ outside the limited range deemed prudent? So you ensure your language extension remains a subset of C++, and write a parser for it. It sounds like a tar pit though.

    Paul

  7. And if struct snark has a function pointer member called “conjugate?” Which one do you pick?

    1. >And if struct snark has a function pointer member called “conjugate?” Which one do you pick?

      The structure member, for maximum downward compatiblity. But the compiler should throw a warning.

  8. This raises the question of why bother with the s.conjugate(…) or b.conjugate(…) syntax in the first place. I would take things in the other direction — remove the silly obj.method(arg1, …, argn) syntax from OO languages and replace it with method(obj, arg1, …, argn).

  9. Also conspicuous by its absence: polymorphism of any sort. The whole reason to have methods, let alone method syntax, is that the same name — called with the same args — can select one of several implementations to invoke depending on the type of this. What does this syntax extension buy you, exactly, besides a little syntactic sugar?

  10. What you propose is a neat hack and while it has a virtue of being 100% backwards compatible with a plain C compiler, it is just sugar as far syntax goes. The only benefit is better self documentation of code. That nothing to scoff at. I been developing/maintaining a CAD/CAM application since the mid 80s and been using an object oriented design since 1998. Having to read code written in 1986, 1990, etc has hammered in for me the importance doing self documenting code well.

    But in terms of harnessing the advantages of object oriented programming it fall very short. For any language to be useful for my application and design it has to support at minimum

    1) A structure with subroutines where the variable scope is limited to the private and public members of that structure.

    2) the ability of any structure to implement an another structure as an interface and be considered to be same type as the interface structure.

    The above two are the minimum needed to implement what I do in a object oriented design. Inheritance and other object oriented concepts don’t enter much into what I do. As a general comment I cringe whenever I see a OOP tutorial begin with inheritance as the lead concept with the a Dog is a mammal is a animal example. Inheritance is one of the least importance features of OOP compared to interface.

    For example if you look at the GoF (Gang of Four) book Design Patterns, the vast majority of patterns use interfaces not inheritance.

    If you can come up with a 100% backwards compatible to C way of doing interfaces now that would be a neat hack

  11. Robert Conley: What you propose is a neat hack and while it has a virtue of being 100% backwards compatible with a plain C compiler, it is just sugar as far syntax goes. The only benefit is better self documentation of code.

    I tend to agree with Robert here. I’m always leery of attempts to do object-oriented things in a non-object-oriented language. If you want objects, use an object-oriented language; don’t try to do object-oriented things in a language that was never designed for it.

    In this case, as Robert pointed out, it has the benefit of self documentation – but it’s also misleading because it pretends something is an object when it’s not.

    1. >but it’s also misleading because it pretends something is an object when it’s not.

      That depends entirely on what other OO features it is implemented with. For example, suppose we were to re-use C’s forgotten keyword ‘entry’ to declare Go-style interfaces. Suddenty, then, we would have a reall object system.

  12. I think your syntax isn’t one-to-one; that is, couldn’t you run into cases where one statement using the new syntax maps to multiple existing statements? Then the compiler has to either catch this possibility or implement some kind of associativity and order-of-operations rules (It’s been a while since I’ve used C, hopefully I don’t embarrass myself with a stupid mistake).


    struct snark {
    int myvalue;}

    void plusequal (struct snark * leftval, struct snark * rightval) {
    (*leftval).myvalue += (*rightval).myvalue;}

    struct snark foo, bar;
    foo.myvalue = 1;
    bar.myvalue = 1;

    foo.plusequal(&bar);

    Does the last statement map to the first argument having been omitted
    plusequal(&foo, &bar); or the second plusequal(&bar, &foo);?

    1. >Does the last statement map to the first argument having been omitted

      That’s probably the most natural way to resolve the ambiguity. It is irritating that we need an extra rule here, though.

  13. > If you can come up with a 100% backwards compatible to C way of doing interfaces now that would be a neat hack

    I think all you’d really need for this would be to extend the extension in the OP to function pointer members of the structure.

    i.e.

    struct foo {
    void (*bar)(struct foo *this, int arg);
    }

    void baz(struct foo *foo1) {
    foo1->bar(42); //now: foo1->bar(foo1, 42)
    }

    At that point, your structure is basically a virtual method table.

    1. >At that point, your structure is basically a virtual method table.

      While that’s true, it’s not responsive to the question I think Robert Conley was trying to ask. I think he meant “interfaces” in the sense of declarations of existential types, the way Go has them.

  14. What makes a interface useful is the following characteristic

    You have a structure/object that handles file transfers

    Interface IFileTransfer {
    Sub Initialize()
    Function SendFile(filename as String) as Integer
    Function ReceiveFile(fiename as String) as Integer
    Sub Terminate()
    End Interface

    Class TCP_IP_Transfer
    Implements IFIleTransfers
    (followed by the implementation of the above methods and properties

    Property Address as String
    (followed other class specific routines for setting a TCP IP connection.
    End Class

    Class Serial_Transfer
    Implements IFIleTransfer
    (followed by the implementation of the above methods and properties

    Property ComPort as String
    (followed other class specific routines for setting a TCP IP connection.
    End Class

    Hope that clears up what I mean by interfaces.

    I have strong opinions about this because I had to drag my company’s CAD/CAM software from a 1980 300 series HP Workstation, to a work alike environment under MS-DOS to Windows 3.0 (16-bit), then to Window XP (32-bit) and it successors. Along with interfacing all of this with various low level motion controllers, PLCs, and electronics using assembly and domain specific langauges.

    My objective during the transition to 32-bit, circa 1997, is to design the core in such a way that it doesn’t have to change in design regardless of how the platform changes.

    In large part I was successful because I designed it around a set of interfaces. As long as the successor platform and language support interfaces (and object encapsulation). I can get from where I was to where I need to be in a orderly series of changes with complete backwards compatibility in regards to file format.

    It also allows me to support a half dozens distinct programs running and designing for our metal cutting machines as they all run off the same core libraries but with vastly different user interfaces.

    I am a bit of a stickler when it comes to interfaces and encapsulation. Inheritance, generics, and polymorphism can be useful but are not as vital.

  15. C++ has been trying to do something similar (“Unified Call Syntax”) for a few years and may never get it. It’s a hard problem in C++ because there’s so much baggage with namespaces and such. C, of course, lacks that baggage, so it would be easier. On the other hand, C lacks that baggage because they don’t do stuff like what you’re suggesting.

  16. >Does the last statement map to the first argument having been omitted

    That’s probably the most natural way to resolve the ambiguity.

    It’s going to cause some fun debugging if you also want those interfaces you keep hinting at. Consider this example:


    void do_something(struct snark * returnval, struct boojum * argval) {
    (*returnval) = some_operation(argval);}

    struct snark foo;
    struct boojum bar;

    bar.do_something(&foo);

    If snark and boojum implement interfaces such that they may be treated as each other, then your rule requires that the faux-member variable be used as the first argument, so the final statement gets interpreted as do_something(&bar, &foo);. If they don’t, it must be interpreted as do_something(&foo, &bar); because no other arrangement satisfies the ordering of argument types.

    When somebody says, “In the last build we have these really weird bugs, but we can’t figure out what caused the regression,” how many hours are wasted before someone realises, “We updated libsnarkboojum and in the new version snark and boojum implement additional interfaces.”?

    I wonder if we were meant to discover this situation as a test, given the structure names you used and the final line of the poem:

    For the Snark was a Boojum, you see.

  17. I don’t understand why the declaration of a snark named s and a boojum named b implies that s.conjugate should have Jack Schidt to do with b, nor b.conjugate to do with s. What if there are also snarks named t and u and boojums named c and d? How do you magically know which unnamed snarks or booja should be ASS|U|MEd by your new extension?

    However, these:
    s.conjugate(1, 2, &b, 3, 4) -> conjugate(1, &s, 2, 3, 4)
    b.conjugate(1, &s, 2, 3, 4) -> conjugate(1, 2, 3, &b, 4)
    s.b.conjugate(1, &s, 2, 3) -> conjugate(1, &s, 2, &b, 3)

    make sense to me.

  18. esr> 3. Same function can be a method of multiple structures without the requirement that you make an article commitment to which one “really” owns it. (I find this comes up remarkably often.)

    Is that somewhat like generics in C++, a.k.a template functions?

    1. >Is that somewhat like generics in C++, a.k.a template functions?

      No. The kind of joint method I’m talking about isn’t generic across types, it’s more that what type owns it is debatable.

      For example, in reposurgeon there are both Repository and Commit types. A Repository is, in part, a list of Commits. Now let’s say that you want to write a function that, given a commit, tells you its index in the commit list of its repository.

      With conventional methods, you could think of this as a method of Repository that takes a Commit argument, or you might think of it as a method of Commit that takes a Repository argument (perhaps implicitly if the Commit object contains a pointer to its Repository).

      In reality, this is a function on the relation between Commits and Repositories, rather than a function on either type alone. It would nice to be able write one IndexOf(r, c) and have it implement both of the method calls c.IndexOf(r) and r.IndexOf(c).

  19. > No. The kind of joint method I’m talking about isn’t generic across types, it’s more that what type owns it is debatable.

    Why do we even need this notion of “method”, when we don’t have anything close to inheritance and ad-hoc polymorphism? In normal OOP, we care about “methods” persay, as opposed to subroutine calls, because these are precisely the calls that could be replaced in a derived version of your object, and hence a “method” call always involves a kind of ‘dispatch’ (though that dispatch may be a wholly compile-time thing, absent ad-hoc ‘polymorphism’). Isn’t it really just a subroutine call, otherwise? (You could reply that the “object” you’re taking as an extra argument should not be conflated with the “meaningful” arguments of your subroutine call, but the same thing happens when using C subroutine calls to implement closures/first-class functions – you need to pass the “environment” object as an extra argument for the subroutine code to operate on, and perhaps dispatch on that argument just as you would in a polymorphic “method” call.)

    1. >Why do we even need this notion of “method”, when we don’t have anything close to inheritance and ad-hoc polymorphism?

      Because you might have existential types. That is, you might be able to declare functions that take “anything with the following set of members/methods”. It’s like Python duck typing, but checked at compile time.

      One of the enlightening things about studying Rust and Go is that this turns out to be enough. You don’t need inheritance to have a useful object system – which, actually, one might have known from Self, but the Go/Rust demonstration is more practical.

  20. > That is, you might be able to declare functions that take “anything with the following set of members/methods”.

    Typeclasses improve on this by isolating the notion of “set of members/methods” and making it independent from a single “object” argument. (I think Rust trait are sort of t he same thing, but I’m not quite familiar with that.) In C terms, you’d want to implement this kind of thing even at run time, by turning each “set of members/methods” into a dictionary of pointers to subroutines (and perhaps associated pointers to closure environments), and having every call to an “interface” dispatch through a dictionary. (The compiler is then responsible for eliding the dispatch when the “dictionary” happens to be known at compile time). It might be somewhat cumbersome in a C context, but that’s kinda par for the course, and the increase in flexibility compared to e.g. object vtables is quite invaluable.

  21. > Because you might have existential types. That is, you might be able to declare functions that take “anything with the following set of members/methods”. It’s like Python duck typing, but checked at compile time.

    So like C++ templates. For certain values of “declare”.

  22. For those of us who used Microsoft COM or read Design Patterns, we have known for twenty years that you don’t need inheritance to have a useful object system.

    COM is the better known example but the bias against Microsoft, as well as Visual Basic 5 & 6 in many of the open source communities and the dominance of how C++ handled object oriented programming pushed this to the sidelines. COM doesn’t have inheritance and solely focuses on implementation of interfaces. If you want to inherit behavior you have to do object composition.

    As to why I mention Visual Basic most don’t realize that Visual Basic 6 is a thin layer over COM. That when it comes to object oriented programming everything in VB6 (or 4 or 5 for earlier versions), has a one for one correspondence to what COM supports.

    1. >the dominance of how C++ handled object oriented programming pushed this to the sidelines.

      I think that’s sufficient to explain it, really.

  23. > For example, in reposurgeon there are both Repository and Commit types. A Repository is, in part, a list of Commits. Now let’s say that you want to write a function that, given a commit, tells you its index in the commit list of its repository.

    I think that the commit list should be ‘private’ to the repository, and managed via methods including a ‘find’.

    > and the dominance of how C++ handled object oriented programming pushed this to the sidelines.

    I think C++ simply implemented early OOP dogma. The misuse of inheritance follows.

    > COM doesn’t have inheritance and solely focuses on implementation of interfaces.

    I never understood the objection to the lack of inheritance, although of course lots of COM interfaces are specified in terms of others using inheritance, and then implemented internally using inheritance.

    I also don’t understand the fundamental objection to C++. Just don’t take aim at your foot.

    Paul

  24. @Paul R
    I programmed a add-on for the Orbiter Space Simulator (link below) in C++ that modeled every switch and most of the subsystems in the Mercury Space Capsule.

    I was familiar with C/C++ from my work in Motion Control and low level hardware control. However my primary langauge was Visual Basic 6 which I used to implement a sophisticated CAM.

    When I got involved in developing the Mercury Add-on I came up with a for of a state machine that modeled each major phase of a Mercury spaceflight as it own state. This allowed me to write the launch phase with the focus on the Atlas rocket with a different set of logic than the orbital phase when it was the capsule alone.

    Now if I did this in VB6 there I would be using different types of interfaces. However in C++ I quickly found that the C++ use of virtual interfaces was a half-ass way of trying to get OOP style interfaces up an running. It really focused a lot on on inheritances and had a lot of kluges when it came to implementing multiple interfaces for a single class.

    Now because it C++ it wasn’t that bad, it had been hammered and honed to a usable point by the early 2000s. But compared to VB6 it was a stupid way to go. And the fundamental sin was it focus on inheritance over implementing interfaces.

  25. I do a lot of C++ coding but since C++11 I almost never use inheritance any more. Function members are more flexible for dynamic dispatch use cases, and unlike the built-in virtual functions you can change them after the object has been constructed. Templates and overloading take care of static dispatch use cases. Even generic pointers and variant types are easier to do with template classes than with virtual ones.

    The two major use cases I still have for inheritance are to wrap C structures with static methods (i.e. fill the struct from the constructor) and to make derived classes from std::exception.

  26. > And the fundamental sin was it focus on inheritance over implementing interfaces.

    I think accidents of history when CS was fumbling forwards. Stroustrup’s example of virtual members in ‘D&E’ is of deriving ‘circle’, ‘square’ etc from ‘shape’ so they can all implement ‘draw’. Which is really an example of an interface without the word being used.

    Anyway ISTM that if Eric is looking at a ‘C with Classes’ renaissance then taking a look back seems the way to move forwards. You could imagine a relatively lightweight pre-processor that produces an output for a C++ compiler but constrains the feature set.

    I think I’d rather pursue bleeding edge C++ though. Templates, lambdas, auto and the STL with a side-order of Boost. For this type of thing anyway.

    Paul

  27. Before you go much further down this road, you should take a look at how Brad Cox implemented objc_msgSend(id self, objc_selector _cmd, …). “self” is the receiver, _cmd is the method selector (an ASCII string in practice), and the contents of the rest of the argument list were up to the particular method to suss out.

    1. >D also supports what you propose

      Yes, I found out about this after writing the OP. If I’m not mistaken, though, D only supports folding on the first argument, not later ones.

  28. The problem being that C doesn’t support function overloading (and for good reasons), which means that once one “class” has a method named foo, no other “classes” can anymore. Same problem classic C enums have, though it would be much more painful here.

  29. There is no problem of overloading for what esr wants. Nor is it anything like C++ generics.

    Given a function foo( bar a, baz b );, all he wants to do is call that function using only one of the parameters while the other is substituted. e.g. For a pair of variables, bar A and baz B, either one of:

    A.foo( B );
    or
    B.foo( A )

    While both would call the same foo() function, I am still left wondering why anyone would want this. The compiler complexity to support this would not seem to equal any supposed gains (whatever those are)?

Leave a Reply to J. Random Hacker Cancel reply

Your email address will not be published. Required fields are marked *