Polymorphism and Heap Allocation

For a while (until about 2 and a half minutes ago), I thought that if I had a base class B, and two derived classes D1 and D2... And I wanted to treat D1 and D2 polymorphically as if they were B's I would have to use a pointer to B and use 'new' like so:

class B {
  public:
    virtual void execute() {}
};

class D1 : public B {
  public:
    void execute() override { Serial.println("D1"); }
};

class D2 : public B {
  public:
    void execute() override { Serial.println("D2"); }
};

void test(B* b) { b->execute(); }

void setup() {
  Serial.begin(115200);
  test(new D1());
  test(new D2());
}

void loop() { }

But alas in the Arduino world we fail to appease the Gods of Memory Allocation because the D1 and D2 are allocated on the heap and the pointer gets scoped out. As I understand it, this is bad.

Does this second example (which for all intents and purpose, functions the same) have any big Arduino-Faux-Pas?

class B {
  public:
    virtual void execute() = 0;
};

class D1 : public B {
  public:
    void execute() override { Serial.println("D1"); }
};

class D2 : public B {
  public:
    void execute() override { Serial.println("D2"); }
};

void test(const B& b) { b.execute(); }

void setup() {
  Serial.begin(115200);
  test(D1());
  test(D2());
}

Can someone explain the key differences between the two and the how and why it still works without needing to use 'new'? And in which case, why in the C++ world outside of Arduino do most of the example codes I see favour new over a 'const &'

It seems the second example is the (hopefully!) perfect solution to the frustration I've had so far with runtime polymorphism using Arduino.

you have much more heap memory and better memory management outside of Arduino

What about your first code didn't "work"?

There are three differences. The first is where the two instances are stored. Heap for the first version. Stack for the second. That's it.

The second difference is your use of a reference versus a pointer. Essentially, a reference is a pointer that cannot be NULL (with a very tiny amount of syntax sugar). That's it.

The third difference is your use of const. I have no idea what effect applying const to the B type is going to have. Given the fact that execute is not also const I assume the net effect is that b cannot be reassigned inside of test.

"Gets scoped out"? Do you mean "goes out of scope"?

And, since the heap instances are never deleted ...... Memory Leak.

Calling a non-const member function on a const object causes compiler warning.

The const was me throwing poop at the wall to see what sticks and then googling compiler errors messages in search of solutions. Adding const was the solution to the following error:

/sketch/sketch.ino: In function 'void setup()': /sketch/sketch.ino:20:8: error: cannot bind non-const lvalue reference of type 'B&' to an rvalue of type 'B' test(D1());

Re: Scoped out. Yes I do. My name is way cooler :sunglasses:

Yes, not calling delete wasn't intentional here, but mostly as the question was more about creating the instances.

Ah. I think I get it. This... test(D1()); ...creates a temporary instance of D1. My guess is that the compiler is allowed to place temporaries wherever it wants including entirely in registers. I also assume that the compiler is not expected to be able to provide references to temporaries; that would fall somewhere between problematic and not possible. If that's true then, by the time of the actual call to test, the temporary D1 may not exist. (In other words, a scoping problem.)

The solution is trivial. Let the compiler know the two instances should be on the stack...

void test(B & b) { b.execute(); }

void setup() {
  Serial.begin(115200);
  D1 d1;
  test(d1);
  D2 d2;
  test(d2);
}

My guess is that the const forces the compiler to move the temporary to the stack.

'const' really doesn't have any thing to do with storage location .... stack, register, or otherwise.

When an argument is passed by const reference, it prevents the function from modifying the referenced variable.

When const is applied to a class member function, it's your promise to the compiler that said function won't change the object's internal state.

You can pass a "temporary" value (in this case it's really an rvalue) by const reference.

This compiles error / warning-free and works just fine:

class B {
  public:
    virtual void execute() const = 0;
};

class D1 : public B {
  public:
    void execute()  const override { Serial.println("D1"); }
};

class D2 : public B {
  public:
    void execute() const override { Serial.println("D2"); }
};

void test(const B& b) { b.execute(); }

void setup() {
  Serial.begin(115200);
  delay(1000);
  test(D1());
  test(D2());
}

void loop(){
}

You can also overload functions to get different behavior depending on whether you're passing an rvalue or lvalue:

class B {
  public:
    virtual void execute()  = 0;
};

class D1 : public B {
  public:
    void execute() override {
      Serial.println("D1");
    }
};

class D2 : public B {
  public:
    void execute()  override {
      Serial.println("D2");
    }
};

void test(B&& b) {
  Serial.print("rvalue reference -->");
  b.execute();
}
void test(B& b) {
  Serial.print("lvalue reference -->");
  b.execute();
}

void setup() {
  D1 d1;
  Serial.begin(115200);
  delay(1000);
  test(D1());
  test(D2());
  test(d1);
}

void loop() {
}

Output:

rvalue reference -->D1
rvalue reference -->D2
lvalue reference -->D1

Which is a great explanation and I appreciate that you took the the time to write it but it doesn't actually explain why the original const is necessary.

Sorry, lost track with all the different codes. What do you mean by "original" const?

The only const in the original post.

Because of passing a temporary instance?

This is intriguing and I'm learning bits here, so thank you and please continue :smiley:

Ahhh ... the second code in the original post.

First that code doesn't compile because it's missing the loop() function.

Once that's fixed, removing the 'const' from:

void test(const B& b) {

causes a compiler error because you can't bind a non-constant reference to an rvalue (aka "temporary").

Even with the the 'const', the code still isn't right. It gives compiler warnings because you're now calling a non-const instance function on a const object. The code I showed in Post #11 fixes that.

Why not?

Why yes?

The trivial answer is: "Because the compiler says so."

I think a complete answer requires diving into the world of lvalues, rvalues, and perhaps even more advanced C++ esoterica. I have a good enough working knowledge of these things to apply them without getting into trouble. But, I'd be hard-pressed to come up with a formal explanation. Perhaps others could do that.

After some reading #9 isn't too far from the mark. It's a question of has-storage (lvalue) versus does-not-have storage (rvalue). Without storage it's impossible to create a reference.

Presumably the const allows the compiler to move the value (rvalue) to storage (turn it into an lvalue) (that's how simple const values work with avr-gcc; taking a reference moves the value to storage).