Download

Lacking essential coding utilities in the Blueprint Editor.

My point was that fixing technology is still much easier for the most part than fixing people.
But I agree that fixing people would be the more “proper” solution if its a possibility.

When it comes to inheritance, it’s absolutly not a mistake!
Inheritance is one of the most powerful programming techinques out there.
If you think that is a mistake, I would argue you should look into learning what good you can use it for.

And private/protected is a also very essential part of that.
Without it, you would start crying when you were trying to access the one method you want to use, and find 1000 of simularly named methods available on the dropdown list.
It’s meant to be a clean-up feature. And working in a dirty environment is not good.

Note that I said “implementation inheritance.”

Having worked on actual C++ compilers and having shipped public SDKs based on C++ in the 1990s all the way to today, I know very well how implementation inheritance is used, and in almost every case where it’s used, there exists a simpler to work with, safer to execute, and more performant way of achiving the same goals using a function library, or using interfaces-only, or using type classes, or using type traits, or using delegation.

Anytime you have to ask yourself the question “Do I need to call the superclass function or not? What happens if I do? What happens if I don’t?” then you’re running into one of the biggest problems of implementation inheritance. But there are others (having spent years of my life mitigating the Fragile Base Class problem in practice, for example.)

Go learn some Rust, and some Go, and some Haskell, and ship a real project in each of those languages, and then go back to the Java-style deep implementation inheritance chains, and you’ll realize that most of the implementation-based OO was just GOOP getting in the way of the underlying code that wants to get out.

I have tried looking up some info on “implementation inheritance” but I can’t figure out exactly what you’re reffering to.
I can describe the type of inheritance I know of, which I do like very much…

abstract class Animal has a method called Decicion. This runs based on a random timer.
class Tiger inherits from Animal, and can choose how it wants to implement its Decicion method.

(If this is not what you meant, could you please provide me a example where I can see the problems you reffer to?)

Implementation inheritance in C++ is when you have a class that has both:

  • non-abstract functions
  • virtual functions

struct Vector3 { ... };

This is a value object, with no virtual functions, and can work fine as a container of plain data. (Maybe even use unique pointers, shared pointers, and such.)


class IMyInterface {
  public:
    virtual void MyFunction() = 0;
  protected:
    virtual ~MyFunction() {} // avoid compiler warnings
};

This is an interface class. (C++ doesn’t have “interfaces” per se, but these PABC Pure Abstract Base Classes will serve)

Those are both fine.

This is a bad API:



class MyThing {
public:
  MyThing(lots of parameters);
  ~MyThing();
  virtual void DoSomeThing();
  virtual void DoSomeOtherThing();
};


Why is this bad?

Well, someone who overrides DoSomeThing doesn’t know whether they’re intended to call the superclass function or not. It’s also not part of the specification whether DoSomeOtherThing() will call DoSomeThing() or not.

There are more subtle design problems that end up happening as the inheritance chain gets deeper, and the pressure you feel towards creating a “god object” base class that has all the functionality comes from the problems of implementation inheritance.

A better alternative is to use the object simply as a collection of API functions (like a module) and use specific hooks/callbacks where necessary (this is delegation):



class MyThing {
public:
  MyThing(lots of parameters);
  ~MyThing();
  void DoSomeThing();
  void DoSomeOtherThing();
  void AddDelegate(ISomeThingHook *hook);
  void RemoveDelegate(ISomeThingHook *hook);
};

class ISomeThingHook {
public:
  // if returning false, abort the thing. state is the current number of geegaws.
  virtual bool OnDoingSomeThing(int state) = 0;
};


This more cleanly separates the “implementation of my module on my data” of the base class, from “well defined variation points” in the hooks/delegates.

Still, this ends up with a fair bit of pointer chasing. If you can get away with POD structs and free functions, you’ll be better off, and your software will miss cache less often, too! And if you define your interfaces in terms of “large contiguous vectors of data” rather than “one thing at a time,” you end up parallelizing better, which works well both for SIMD, and for threading, and for GPU ports, depending on what specifically your code is doing.

There are other languages where these ways of designing are less cumbersome and more built-in, leading to less boilerplate. Dropping all subtlety, it approximately ends up as: “Haskell good; Java bad.”

That is quite simple, You run it if you want the default implementation at some point and also something in addition to that.
If the defualt implementation is bad for what you’re trying to achieve then you need to re-implement something that works in your favor.
If either of those are really a problem, then its all a fault of the class designers that did not make the base class generic enough.

This is a somewhat naked skeleton of a case I would argue is very good.



class Enemy {
public:
   int startingHealth = 100;
protected:
   int health = startingHealth;

public:
   virtual void OnHit(Weapon weapon, BodyPart bodyPart) {
      //Most common enemies take more damage from the head
      if (bodyPart == BodyPart::Head) {
         TakeDamage(weapon.damage * criticalModifier);
      }
      else {
         TakeDamage(weapon.damage);
      }
   }
protected:
   virtual void TakeDamage(int amount) {
      //Do default health calculation
      health -= amount;
      updateEventForUI();

      //Death check
      if (health <= 0) {
         Die();
         health = 0;
      }
   }
};

class ShieldEnemy : Enemy {
public:
   int startingShield = 100;
protected:
   int shield = startingShield;

   virtual override void TakeDamage(int amount) {
      //Need to consume shield first
      shield -= amount;

      if (shield < 0) {
         int overflowAmount = abs(shield);
         shield = 0;
         Enemy::TakeDamage(overflowAmount);
      }
      else {
         updateEventForUI();
      }
   }
};


It absolutely should be. A base behaviour should be well documented. You should know exactly when each virtual call should appear.
If I in the example above only have shields on some of my body parts, I would have to account for that with a override to the OnHit method.

Having very deep chains could be a problem.
Though with with a good understanding of the base class as agreed upon by the designers in the documentation.
You should be able to make a decicion on weather you already have a child class that behaves like you want or not.
Ideally, you should never have to go any deeper than 3 levels! (2 Most of the time)

Callbacks are indeed quite useful. Though they should not replace inheritance entirely.
They are useful for decoupling code, like HealthUI in my example.
We want to be able to test the enemy behaviour without needing a specific UI to be there.
We only care to tell the UI that something has happend, and it should update.
How ever, children should in most cases still use the default parent behaviour.
Unless you implement something custom.

Programming is all about making hard problems simple to understand. Being able to break that up into base and child functionality is an important skill for that.
If you do a poor job at generalizing what a base class should do, you get bad code that is hard to work with, that much is true.
But don’t blame that on the language and utilities.
Yes, bad designs is possible because of it. But good designs are also possible because of it.
If we should keep restricting ourselves like this, we would not be able to allow anything for anyone!

But how do I, as a programmer, reading the headers, know whether I do that or not?
Many APIs require that you call the superclass for some functions (Android comes to mind.) But if I get it wrong, there’s really nothing telling me so, until I get some weird runtime failure under hard-to-pin-down circumstances.
I have to trust the documentation, if it exists, or I have to read the source and trust that I don’t miss any subtlety in the inherited code. This adds significant cognitive overhead.

What if there were mechanisms in programming languages that simply didn’t let me do the wrong thing?
Turns out, such programming constructs exist, and they’re not based on implementation inheritance. They can, sort-of, be expressed in C++, although in a very clunky way, using templates and concepts. They can’t even be expressed in languages like Java and C# and Objective-C.

I don’t feel like we’re having the same conversation here.
Ship a couple of systems using other paradigms, and compare.
It’ll be pretty obvious that OO with implementation inheritance ended up being a 20-year detour in the industry’s path towards robust software development.

Yes. The people who should need to work on it should have access to documentation which is at the very least semi-up-to-date.
Full source code so that they can check if anything diverts from the documentation etc.
If you want to work on a system. That system is going to work in a specific way.
You can’t expect to be able to write stuff for that system without knowing full well how it works.

I have seen so many times that people end us jamming wrenches in my cogwheels.
Just because it made the classes functionality pause. Then they come to me when its time to make all the cracked wheels turn again.
With proper hiding of things that should not be messed with, this could not have happened.

If it seems hard to implement what you try to do, without having access to all the underlaying variables and methods that go into a system.
it’t more likely than not, the wrong use case for that system.

Could be that the story is different for other languages and engines.
But Unreal is the only relevant topic at the moment for me.
Why am I seeing such a great improvement in how easy it is to develop after I started using these then I wonder?

This is how it works in a normal software development environment. You often just get headers to compile against and absolutely zero knowledge of the inner workings of the software you’re working with.

Yeah, I have come to understand that things are not the same for software development and Unreal.
Which is why I hope we could lay this discussion on ice.
Because by now, I’m kind of exhausted from it.

This was supposed to be a simple feature request to make blueprint more consistently the same as c++.
I don’t wanna argue weather the guys who invented c++ made the rights calls or not.
You work with what you have, and you make it work consistent.

Actually it has been accepted in 6 weeks :smiley:

https://github.com/EpicGames/UnrealEngine/pull/7119
https://github.com/EpicGames/UnrealEngine/pull/7120