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.
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"?
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());
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...
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.
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.
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).