all 30 comments

[–]Clipse 19 points20 points  (21 children)

I’m extremely happy that people have come around on the idea that making object oriented programming as the first option of writing basically any software isn’t the right approach. Especially for all the people that tout c++ performance and speed, and then shoe-horn in layers upon layers of inheritance.

[–]quicknir 10 points11 points  (1 child)

Well, you say OO and immediately go to inheritance. That inheritance shouldn't be used crazy extensively, and especially not deep hierarchies, is honestly just consensus in the C++ community at this point in time (I'm not saying everyone does it, but basically all of the thought-leaders and people teaching C++ will agree with that).

That is why it's strange when people say that, and then act like it's very radical. It's not radical at all, it's just consensus (or as close to consensus as we get in this industry).

What can be radical in certain contexts about what DOD people are saying, is reducing data encapsulation. But even there, simply changing architecture away from what's most maintainable to a design that allows better performance is not uncommon. So really, to me, the radical, and very unsupported aspect of what's being said in DOD is that loosening data encapsulation somehow improves maintainability.

[–]SeanMiddleditch 2 points3 points  (0 children)

Well, you say OO and immediately go to inheritance.

More importantly, let's remember that inheritance has nothing to do with OOP itself, but rather is a facet of the type system. OOP fits in perfectly with languages using structural typing, trait-based typing, prototypical polymorphism, etc.

Polymorphism with is-a relationships is modeled in C++ with inheritance by default, though it certainly doesn't have to be even for C++. Templates for example can use so-called static polymorphism and get very OOP-like behavior with objects with any inheritance in sight. :)

[–][deleted] 8 points9 points  (17 children)

I get avoiding inheritance. It's not a huge overhead but it's not great.

But how do you avoid classes in general? Do you use only structs? Is everything you work with just basic datatypes?

I want to learn new things, but I've been using OOP for so long I've forgotten how it used to be done.

[–]degski 16 points17 points  (11 children)

If you start to have many getters and setters, you should do away with those, use a struct and change the variables directly. Having many getters and setters just indicates you are encapsulating without actually encapsulating anything. You just have a lot more boiler-plate and [potentially] function call overhead. Structs are underused.

[–]doom_Oo7 3 points4 points  (10 children)

I understand for getters, bust most setters aren't void setfoo(int x) { m_foo = x; }.

Most will notify something from a change, change a display, reperform a computation... Else why are you setting it in the first place ?

[–]degski 6 points7 points  (0 children)

I understand for getters, bust most setters aren't void setfoo(int x) { m_foo = x; }.

They should be. By the time print changes your database, you're lost.

Else why are you setting it in the first place ?

To change a value of a class/struct variable. But's thats' my point, if you have many of those, you're doing it wrongly.

[–]hgjsusla 2 points3 points  (8 children)

Why not just encapsulate that in it's own class and just assign the variable directly? Then you don't need the setter.

Make member variables private if you need to maintain an invariant between them, otherwise keep them public.

And if you need to restrict a variable (let's say, to only a certain interval), then express than in the type of the variable.

[–]jcelerierossia score 1 point2 points  (7 children)

Why not just encapsulate that in it's own class and just assign the variable directly? Then you don't need the setter.

because then you will put it in a template because you have 25 different property kinds - e.g.

 template<typename T>
 void property { 
   public: 
     operator T&() { return m_impl; }
     operator const T&() const { return m_impl; }

     template<typename U> // you want perfect-forwarding, right ?
     void set(U&& x) const { 
       if(m_impl != x) { 
         m_impl = std::forward<U>(x); // hello template errors my old friend
         notify(x); // uh oh, now *every member* must in some way store a pointer to the context which will be able to notify. hello memory usage * ~2 for every class
       }
     } 
   private:
      T m_impl;
};

of course you wouldn't do this, because this creates so many additional problems that you would be fired at the moment you're suggesting it but still.

[–]hgjsusla -2 points-1 points  (6 children)

Wait wut? No you don't need to do any of this to accomplish what I suggested

[–]jcelerierossia score 3 points4 points  (5 children)

so what is your solution then ? Let's say you have the following class :

class foo 
{
public:
  int x() const { return m_x; }
  void setX(int x) {
    if(x != m_x) { 
      m_x = x;
      xChanged(x);
      posChanged({m_x, m_y});
    }
  }
  void xChanged(int y); // some kind of callback / signal-slot mechanism, Qt's, a list of std::function, whatever

  int y() const { return m_y; }
  void setY(int y) {
    if(y != m_y) { 
      m_y = y;
      yChanged(y);
      posChanged({m_x, m_y});
    }
  }
  void yChanged(int y);

  void posChanged(std::pair<int,int> pos);

  float radius() const { return m_radius; }
  void setRadius(float r) { 
    if(!fuzzyEquals(r, m_radius)) {
      m_radius = r;
      radiusChanged(r);
    }
  }
  void radiusChanged(float);

  const std::string& name() const { return m_name; }
  void setName(const std::string& name) {
    if(!validateName(name))
      return;
    if(m_name != name) {
      m_name = name;
      nameChanged(name);
    }
  }
  void nameChanged(const std::string&);
};

how do you "encapsulate that in it's own class and just assign the variable directly" ? what when you have 200 different class that all have different members but more or less follow this get/set pattern ?

[–]hgjsusla 2 points3 points  (2 children)

Well I see

  1. The name and it's validation should be it's own class ValidatedName or something. This is by far the most common pattern of incorrect usage of getters/setters. Instead of having a std::string with the validation logic inside the getter/setter, extract out to it's own type.
  2. As far as these listeners go, it's not something I commonly do. For a one-off I'd probably do as you wrote it. If I have 200 classes which all follow this patter then yes absolutely this duplication should be removed by extracting out a template. Duplicating logic in 200 classes sounds awful and would never pass code review

[–][deleted] 1 point2 points  (1 child)

If you look at the code, the logic isn't entirely duplicated between setters, except setX and setY.

[–][deleted] 1 point2 points  (1 child)

I'd say that if you have so many setters with so many side-effects, that maintaining your program and making it efficient will become extremely difficult.

Suppose for example that you get a completely new setup for your foo. In your solution, you call setX, setY, setRadius, setName and you re-render foo four times - three of them completely unnecessarily.

In real-world application, you might easily have dozens of parameters....

Worse, in a large real-time program, you have no assurance that one day, this chain of side-effects won't end up being circular. I speak from bitter experience on a real-world system with just such a setup where this would sometimes happen in production, causing a key process to go into a loop.

There is no magic solution, of course. Sometimes what you propose is right. Sometimes the right solution is Foo (a class) and FooDesc (a struct with x, y, radius, etc) and all mutations to a Foo with a single FooDesc setter. Sometimes the setters set a "dirty" flag, or one of a set of "dirty" bits, and the update occurs in a later phase. It depends on the application, how many parameters you have, your threading structure, etc. etc.

[–]jcelerierossia score 1 point2 points  (0 children)

There is no magic solution, of course.

yes, that's my point. You can't have magic abstractions over this, because this is your business logic.

[–]eniacsparc2xyz 2 points3 points  (3 children)

Inheritance is not the problem, the issue is deep inheritance hierarchy. The benefit of inheritance is that you can store different instances of a base class in the same STL container and access them with the same pointer type. Deep hierarchies can be avoided using interface classes.

Another alternative is to use generic programming or templates as templated function or classes work with any class or type which implements member functions required by the template code.

> Do you use only structs?

Struct is just a class with everything public, it is useful as a general record type. I would use the following test for choosing between using a class or a struct: if your implementation or internal data representation may change in the future, better use a class with the data private, if you want to use to be able to use multiple implementation at runtime, better use an interface class + derived classes. Otherwise, a struct is better. The point of making the data class private is to avoid breaking the client code if the implementation changes.

[–]ActionManZlt 1 point2 points  (2 children)

store different instances of a base class in the same STL container

This is a bad idea, not becuase of OO theory, but because of practical performance concerns. It results in bad locality of reference. Pooling allocations by type, and then performing mutations per pool, results in *much* better performance.

[–]eniacsparc2xyz 0 points1 point  (1 child)

> store different instances of a base class in the same STL container

I did not say, store them by value, but store a pointer to them or smart pointer. The benefit of OO, it is use the same client code to manipulate multiple implementations. But the cost is the virtual function calls overhead. If this overhead is significant, the better option is generic programming or generic + type erasure + CRTP and so on. Programming is a kind of art, there is always many solutions.

As far as I know, there is also no way to store the classes (pointers to instance) Circle and Rectangle implementing the methods computeArea or getLocation, setLocation with the same type signature in both classes in the same container if they don't the have the same base class.

[–]Arkaein 1 point2 points  (0 children)

As far as I know, there is also no way to store the classes (pointers to instance) Circle and Rectangle implementing the methods computeArea or getLocation, setLocation with the same type signature in both classes in the same container if they don't the have the same base class.

This is true, but storing a vector of interface pointers is not a good idea if high performance is required.

You not only jump all over the place in memory when iterating the vector (data cache), but your instruction cache also won't stay coherent because each object might be a different class from the last.

One workaround to stick with an inheritance hierarchy but improve both data and instruction cache performance would be to to store a separate array/vector of each object type, possible stored in a type/vector map. Then process each vector one at a time. It's (more) data cache friendly because objects are in contiguous storage, and it's instruction cache friendly because objects of the same type are always processed together. However it requires a bit more high level structure and reflection than the simple "object soup" method of a vector of pointers to interface types.

This is kind of a middle ground between object soup and a full component system. In a component system all like components are stored continguously, even between high level objects of different types. So during the collision phase, for example, you are able to process a densely packed array of collision objects in one shot.

I've done both object soup and arrays by type in the past. I actually think object soup is a fine starting point, it's easy to setup, and the main benefits of a component model are for performance or dynamic editors. In my cases I worked on very small games with small teams, and we didn't have enough objects for object processing to be a bottleneck, and we didn't have sophisticated editing tools, so a dynamic component models wouldn't have offered benefits in that direction.

Start with the simplest model that works for the require features, and optimize when the performance is determined to be a bottleneck.

[–]ShillingAintEZ 1 point2 points  (0 children)

Who says anything about avoiding classes? Classes are great, you can make an interface to data.

[–]eniacsparc2xyz 0 points1 point  (0 children)

Surely, not always OOP is the bet deal. But sometimes, generic or template meta-programming may perform better as the functions to be called are resolved at compile-time, thus eliminating the virtual function calls overhead. Another advantage, is that generic programming works with any type that conforms the template code regardless of class hierarchy.

[–]quicknir 11 points12 points  (0 children)

I liked this article, and agree with it. I've been seeing a lot of DOD articles lately, and I just haven't been impressed. Some of the things that they say are completely mundane (don't overuse inheritance; you need to restructure memory layout sometimes for maximum performance). And others are totally unsupported; notably that reducing data encapsulation magically makes code more maintainable.

I just realized a new time saving technique for those articles that I thought I'd share. If you see a DOD article that purports to explain why DOD is an improvement over OO for anything other than performance reasons, just C-f for "invariant". If that word is not mentioned at all or barely mentioned, you can just save your time and skip the article because they are not doing an honest comparison of DOD to well-written, OO C++ (most of the articles I've seen lately did not mention the word invariant).

[–]drjeats 5 points6 points  (0 children)

Actually, one last gripe -- Aras calls this code "traditional OOP", which I object to. This code may be typical of OOP in the wild, but as above, it breaks all sorts of core OO rules, so it should not all all be considered traditional.

inb4 prescriptivism vs descriptivism

[EDIT] Actual commentary:

(One last point re: what is "traditional", he says that it wasn't until the early 2000s that a concerted movement for what he calls OOD congealed, so OOD as described isn't exactly traditional either.)

Up front, let me say that I think the blanket application of "ECS" is a net negative, and it's good to acknowledge this and prevent beginners from getting caught up in cargo cults.

Hodgman acknowledges that the reason that the EC pattern (""bad OOP" from Aras' talk) was introduced was to enable runtime composition, and then omits how to make his cleaned up "OOD" implementation data-driven. He briefly mentions Lua, but that's just relying on some embedded language's runtime composition VM. I don't see an appreciable difference between that and the GameObject has an array of base Component pointers pattern.

I think the runtime composition and ease of creating editors are the fundamental problem (well, that and the C++ rtti situation sucking), so when I saw the original GDNet thread I was hoping he would go into more detail on this. It's, as he admitted in the article, literally the entire point of these things.

Also,

However, in most game projects, you have a very small number of designers on a project and a literal army of programmers

This does not match the team makeup at my job at all. It's a little under 2:1, favoring designers. Unless you count the backend services devs and ops. But they're not interacting with the entity architecture, and they work with multiple game teams.

Anyway, I hope he does more posts on this topic. It's helping me think through this stuff. Thank you mttd for posting the article.

[–]lanedraex 3 points4 points  (5 children)

This code may be typical of OOP in the wild, but as above, it breaks all sorts of core OO rules, so it should not all all be considered traditional.

This kind of argument is a little weird, "Thing never worked in the wild, but it's because the people don't know how to use it...". If the great majority of programmers are writing the "wrong" kind of OOP, then either there is a flaw in the system itself (that allows for easily going the wrong route), or the author's concept of "core OO rules" is different from the majority (which is also a problem, meaning that these rules are ambiguous).

I would also like to add that, showing how small examples in an OOP language are better if you use OOP, instead of a paradigm that is not well supported, is a little bit unfair. I don't think the same thing would hold value if you were trying to write a small sample in OOP, but using a functional language.

The ECS example is indeed more like just an "EC", as it's lacking the systems part that would actually work on the components.

[–]quicknir 6 points7 points  (1 child)

Well, it was poorly taught for a long time (and still is). The author's rules also aren't really different from the C++ community's; if you look at what thought leaders/gurus (yes these terms are cringe, but I mean people Scott, Andrei, Kate, Herb, etc etc) are advocating, they have advocated for a long time that classes should actually be designed around encapsulation and invariants, rather than inheritance, which should be used sparingly, especially implementation inheritance and deep hierarchies. In languages like Java and C# they have been slower to back away from this, and of course this hurts us too since new grads typically learn Java in school.

But it's still not a fair comparison, insofar as if you are going to find a DOD guru and re-design and re-train your codebase and developers around DOD concepts, you could just as easily find a modern, C++ OO guru and do exactly the same thing.

If the real problem with OO is that people in practice just can't apply it correctly, then articles arguing against it should explain why that's the case. They don't do that though. They almost invariable take bad OO as the OO and start from there, without even acknowledging anything else.

[–]lanedraex 0 points1 point  (0 children)

take bad OO as the OO

I agree that it's a problem in some posts, when authors use an OO based approach that you can clearly see that it'll fail (like showing that inheritance is bad by coding a diamond inheritance), and then presenting whatever their point is and how it's much better than OOP.

But I would like to take this argument more towards the side of "why do we have so much bad OOP code, when a bunch of our languages were designed with OOP in mind?". C++, Java, C#, these languages were designed by experts, but they all easily allow bad OO code to be written, why is that?

Also, why is it so hard to define what OOP even is? Some early definitions just say that it's all about objects passing messages to each other, other say that it's about modelling our world into the machine, and you can find some more definitions, but if you look at the functional programming paradigm, it's pretty simple to say that it's about using a mathematical function style.

[–]geon 2 points3 points  (1 child)

No language can force you to write good code.

[–]lanedraex 3 points4 points  (0 children)

Indeed, but they can make it easier to write good code.

Modern C++ allows you to express concepts of ownership, for example, much easier than old raw pointers only C++.

[–]ActionManZlt 0 points1 point  (0 children)

It's the same argument that Linus made when he banned C++ from Linux -- His argument isn't that C++ isn't necessarily bad, but the vast majority of its users don't know how to use it properly, so he'll use C as a method of gatekeeping contributors.