6

In no particular language:

  class fooclass
  {
    int A, B, C, D;
    public int GetItemSum()
    {
      int sum = this.A + this.B + this.C;
      this.D = sum;
      return this.A + this.B + this.C;
    }
  }

compared to:

  struct foostruct
  {
    public int A, B, C, D;
  }

  int GetItemSum(foostruct ref fs)
  {
    int sum = fs.A + fs.B + fs.C;
    fs.D = sum;
    return sum;
  }

Is there a deeper difference than syntax and readability? Is OOP essentially groups together sets of functions where the object reference is always an argument?

The reason I ask is because I came across some C code that was written this way (essentially using Structs as classes), and I'm still having a hard time understanding if there's an advantage to doing it that way instead of just using C++.

  • "there's an advantage to doing it that way instead of just using C++" There isn't. Under the hood, C++ does the same thing. Member methods just act like functions that take this as an implicit parameter. – Alexander Jun 26 '17 at 20:27
  • Please don't forget the semi-columns missing the closing brace of struct or class. and don't forget the column after public. after the – Christophe Jun 26 '17 at 21:12
  • 2
    For C programmers, the advantage to doing it that way is that you can use C. (You may as well ask why they didn't just use Java or Python) – immibis Jun 26 '17 at 23:35
  • 2
    The advantage is that you don't need a C++ compiler. – D Drmmr Jun 27 '17 at 13:42
8

This depends a lot on your definition of OOP. If OOP for you is only about encapsulation and modularization, there's no real difference between your examples. But there are other interpretations:

  • OOP is about “message passing”
  • objects are the combination of data + behaviour
  • OOP is about “subtype polymorphism”

Those all mean slightly different things, but have a common point: when you use an object, you don't need to know how it is implemented. In many languages, this abstraction is managed via dynamic dispatch.

Here we have a consumer of your object:

DoSomething(foo object) {
  Print(object.GetItemSum());
}

Since this is a method call, it is the responsibility of the object to figure out how to get an item sum. I.e. we tell the object what to do. This means the behaviour can be overridden in a foo-subclass. In contrast, if we had used an ordinary function call GetItemSum(object), we would have told exactly how the item sum should be calculated – by exactly that function. Since the caller decides which function to use, a foo-subclass cannot override this behaviour.

Dynamic dispatch can be implemented by putting function pointers to the method implementations into the object. For class-based OOP languages, the function pointers are not in the object directly, but in a method table that is shared by all objects. This table is also called a vtable (virtual method table).

So using pseudo-C/C++ as a language, we can simulate a Java-style class like this:

struct foo_vtable {
  int (*GetItemSum)(foo* this);
};

foo_vtable foo_class = { .GetItemSum = foo_GetItemSum };

struct foo {
  foo_vtable* class;
  int A, B, C D;
};

foo* foo_new() {
  foo* this = malloc(sizeof(foo));
  this->class = &foo_class;
  foo_init(this);
  return this;
}

void foo_init() {
  this->A = ...;
  ...
}


int foo_GetItemSum(foo* this) {
  ...
}

The virtual method call instance.getItemSum() is then actually compiled like:

instance->class->GetItemSum(instance);

That above line is basically the entire magic of OOP. Calling a method indirectly like this is called “dynamic dispatch” or a “virtual method call”.

We can now define a class bar that inherits from foo:

struct bar_vtable : foo_vtable {
  // may or may not have extra methods
};

bar_vtable = { .GetItemSum = bar_GetItemSum };

struct bar : foo {
  // may or may not have extra fields
};

bar* bar_new() {
  bar* this = malloc(sizeof(bar));
  this->class = &bar_class;
  bar_init(this);
  return this;
}

void bar_init(bar* this) {
  foo_init(this);
  ...
}

int bar_GetItemSum(foo* this) {
  ...
}

Now if the instance is created with bar_new(), the instance will use a different method implementation than foo instances. However, any code that uses foo instances through the dynamic dispatch mechanism doesn't have to be updated – because of the dynamic dispatch, the subclass method will be executed instead! The instance itself knows which method to use.

(In another answer, I've written a simpler example of class-based dynamic dispatch that actually runs in C, but it doesn't cover instance initialization. On my blog, I have an article about the differences between dynamic and static dispatch which includes a small runnable C++ example that illustrates these differences, without going into implementation details.)

So what's the point of all this extra code?

  • The Design Patterns book contains many examples how OOP techniques (= dynamic dispatch) can be used to implement simple solutions to recurring design problems. None of those work if you call a function directly, you do need dynamic dispatch.

  • An interface is just a vtable without providing implementations. Whenever you use an interface in your code, you are asking for dynamic dispatch. For example, this enables dependency injection, or easier unit tests via mock objects.

5

There is no functional difference. When you define a method to be part of a class what really happens is the method is placed somewhere in a memory actually taking the class instance it has been defined on as one of its parameters.

I.e. in machine code it does not matter. But where it matters is during development. OOP has become popular because it made defining context boundaries much clearer, through classes. Logical units have been grouped in classes containing not only common properties but also common methods operating on the properties.

Which approach is better is really hard to tell. In functional languages the second way is preferred, having functions transforming input to output, in languages like C# or Java you're more likely to see the first approach. Then there are other developer groups, such as domain driven design evangelist, who would tell you the second example is an Anemic domain model and is useless.

Rather than trying to figure out what's best it's better to pick one style and be consistent. Consistency is predicable.

4

Polymorphism. Can you make a same function name behave in different ways depending on the structure being sent as parameter?

This is basically the difference between OO and plain functions.

So, in C you can't have polymorphism with simple functions receiving structs, but you can have it in C++ by defining methods in classes.

However, some Functional languages allow functions to have different behaviors depending on the type of arguments(i.e. Clojure), so in this case specifically, they'd be equivalent.

  • 1
    To play the devil's advocate, you could have a function pointer in the struct (or a pointer to another struct of function pointers), that refers to that function. In this case you could emulate polymorphism. But you wouldn't of course have easy inheritance, nor constructors, destructors, copy constructors and all the rest that makes C++ so much more powerful :-) – Christophe Jun 26 '17 at 21:19
  • @Christophe Yes, I am aware, but as you said, that would be an emulation and not a first class feature of the language. – MichelHenrich Jun 26 '17 at 22:32
  • 1
    Do you mean dynamic dispatch? Because static overload resolution works with free functions as well in C++, and they are better for encapsulation and extensibility. – Deduplicator Jun 26 '17 at 23:53
2

At the implementation level, what you describe is roughly what's going on in OOP languages. amon's answer goes into this in more detail. If you don't care about types, then that's basically the full story. This is part of the reason there's a hundred and one object systems in Scheme, and it's easy to roll-your-own in dynamically typed languages that have some notion of first-class functions or even second-class function pointers.

When you factor in type checking, things get quite a bit more complicated and pretty strong distinctions start appearing. As a simple example of this, let's say you have a class with some "private" instance variables. If you make this into a struct in C, where are you going to put the private variables? If you put them into the struct, then they aren't really private and you can't pass some other struct with different private variables but the same public interface to functions that accept your type of struct. The alternative is to have a void * pointing to the object's private state. This is ugly but the best you can do and what is actually done in C. What you'd actually want to do is use an existential type. Something like:

struct Foo<S> { // For efficiency reason's, you'd want an approach like amon's
    S *internalState;
    int A, B, C, D;
    int (*GetItemSum)(Foo<S> *this);
}

typedef (exists S.Foo<S>) Foo; // Roughly, Java's Foo<?>

If your language have higher order functions, then this existential is already bound up in the notion of function type, and we can avoid even the ugliness of the internalState field. We can just do:

struct Foo {
    int A, B, C, D;
    Func<Foo *, int> *GetItemSum;
}

But this still has a problem. If I make a Bar struct that meets the same interface but maybe has some extra stuff, an inheritance-like situation, GetItemSum still needs to be a Foo accepting function, not a Bar accepting one. Really, what we'd want to say is that the first parameter of GetItemSum should match the type of the struct containing it. The construct that does this is called self-types. A simple form of it is essentially built into OO languages, and the term usually refers to more elaborate forms. There are a couple of ways to dodge this problem. First, we could move the methods out of the struct like in the code in the original question, but this just moves the problem and makes it so you can't override methods. Alternatively, we could simply have GetItemSum close over the this parameter as well, that is have Foo be:

struct Foo {
    int A, B, C, D;
    Func<int> *GetItemSum;
}

This works until you want to do subclassing, i.e. implementation inheritance. If Foo had another method, say PreProcess, that was invoked by GetItemSum then this approach would lead to the following problem. If I made a subclass Bar which overrode PreProcess, that is replaced it with it's own copy, but otherwise delegated to Foo's implementations, the GetItemSum method would not call Bar's PreProcess but Foo's. This is why open "recursion" comes up often when talking about OO languages. The last encoding, where GetItemSum takes no parameters, requires the "constructor" to recursively define Foo in terms of itself. To allow methods to call methods that might be overridden in a subclass requires this recursion to remain open.

Anyway, type-preserving encodings of object oriented programming into, typically, typed lambda calculi were studied pretty heavily in the '90s. Most of the complexity was caused by things like subclassing and method overriding. The fully featured versions of these encodings were typically too heavy-weight to be seriously used. The encodings (even some of the simpler ones) do illustrate how much complexity is hidden behind the "basic" notions of OO.

It's common for people to talk about how "easy" it is to encode OO into, say, functional languages, but they tend to either a) ignore the types, or b) use a very simplified notion of OO. The good news, though, is that many of the most difficult to encode features are less popular nowadays, such as implementation inheritance. There's also benefits to breaking OO notions down into simpler concepts as some problems, such as the binary method problem, are easier to deal with given the extra flexibility of having the pieces making up the notion of "class".

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.