I'm trying to use a polymorphic multidimensional array of structs and I'm running into an issue.
I am doing something wrong casting or I've made a mistake in the structure design.
if (var_type == 0) {
var_long current = dynamic_cast<var_long*>(*vars[var_type][vars_used[var_type]]);
current.value = atol(input_buffer);
Serial.println(current.value);
} else if (var_type == 1) {
var_float current = dynamic_cast<var_float*>(*vars[var_type][vars_used[var_type]]);
current.value = atof(input_buffer);
Serial.println(current.value);
} else {
var_string current = dynamic_cast<var_string*>(*vars[var_type][vars_used[var_type]]);
strcpy(current.value, input_buffer);
Serial.println(current.value);
}
Gives me these errors:
Arduino: 1.8.15 (Windows 10), Board: "Arduino Uno"
C:\Users\anon\Documents\Arduino\junk\junk.ino: In function 'void loop()':
junk:61:52: error: no match for 'operator*' (operand type is 'var')
var_long current = dynamic_cast<var_long*>(*vars[var_type][vars_used[var_type]]);
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
junk:65:54: error: no match for 'operator*' (operand type is 'var')
var_float current = dynamic_cast<var_float*>(*vars[var_type][vars_used[var_type]]);
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
junk:69:56: error: no match for 'operator*' (operand type is 'var')
var_string current = dynamic_cast<var_string*>(*vars[var_type][vars_used[var_type]]);
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
exit status 1
no match for 'operator*' (operand type is 'var')
This report would have more information with
"Show verbose output during compilation"
option enabled in File -> Preferences.
I seem to recall reading in the forum that dynamic_cast is not enabled (in at least some Arduino cores) because of its large runtime footprint.
That being said, I also think that you're using it incorrectly. My rudimentary understanding is that dynamic_cast is used to cast a base class pointer into a derived class pointer. Runtime processing is needed for this because it must be determined if the pointer does indeed point to an object of the correct derived class.
First by doing vars[0] will give you back a var pointer, e.g. var *. If you use the array subscript (operator[]) on that again you will be accessing memory w.r.t. to the size of struct var. Since the size of struct var is not the same as struct var_long (or one of the others) this will be undefined behavior.
struct V
{
virtual void f() {} // must be polymorphic to use runtime-checked dynamic_cast
};
Besides that, like gfvalvo pointed out, it looks like the -fno-rtti flag is enabled, this prohibits the use of dynamic_cast.
Edit: I would like to add that you don't have to use dynamic_cast to downcast from base to derived. A static_cast will do just fine. The problem with a static_cast is that it won't check if a given type can be downcasted to the derived type. E.g.
struct A
{
virtual void foo() {}
};
struct B : A {};
struct C : A {};
struct D {};
int main()
{
B obj_b;
A * pointer_to_b = &obj_b; // Implicit cast (fine since were upcasting and the types can be checked at compile time)
C * pointer_c_safe = dynamic_cast<C*>(pointer_to_b); // This would've returned a NULL since dynamic_cast checks if the object pointed to by pointer_to_b is of type C, which it isn't
C * pointer_c = static_cast<C*>(pointer_to_b); // Will work just fine, but undefined behavior since pointer_to_b points to type B and not C
B * pointer_b = static_cast<B*>(pointer_to_b); // This is all fine, its up to the programmer to ensure that pointer_to_b actually points to an object of type B. In this example it does, no undefined behavior!
D * pointer_d = static_cast<D*>(pointer_to_b); // Compile error since type A cannot be casted to D
}
Maybe you can do some research into the union data type and see if that might be a useful tool to your problem. A union allows you to store multiple types into one memory location, e.g.
union my_union
{
int by_int;
char by_char;
};
my_union foo;
foo.by_char = 'H';
my_union bar;
bar.by_int = 12345;
With the cast itself is nothing wrong, the compiler won't complain and you should be able to run your program. However if pointer_c gets dereferenced, it will be undefined behavior, anything can happen after that :-).
I am trying to eliminate as much redundant code as possible. This redundancy is because of the strongly typed nature of C++. I'm thinking that overloaded functions is probably the way to go in the end but it would be ideal if I could do something like this: (There are errors in this code)
In your case they nearly are. An int32_t and a float take 4 bytes each. And, a String only takes about 6 bytes, I believe .... not counting the dynamically-allocated c-string. So:
Trying to avoid the use of dynamic memory as I plan on using just about every bit of it, and a couple string allocations will certainly shred it.
However, the code your provided is quite elegant and does the job albeit in a different way than I wanted to. Thank you.
Any way to point those to a preallocated char[]?
They also definitely need their names, which it looks like you eliminated.
As I told a poster in a different thread, my go-to solution for that is to throw better hardware at the problem. Hardware is CHEAP, pulling your hair out is painful.
Why do you think that? If all you do is grab the memory (when the String is created) and hold it the entire time without de-allocating or resizing, then what would cause dreaded, so-called "shredding"?
You could define the char array size in the struct. But then all instances of the struct must be that large. Or you could handle the dynamic array yourself. But, that would only save a couple of bytes over using a String.
Same choices as with the "String" variable type. Either define a one-size-fits all char array for the name or dynamically allocate just the size you need for each name.
But if (for what ever reason) you're stuck on a low-resource AVR, then you could keep the c-strings in PROGMEM. The downside is more clumsy initialization as it's a 2-step process (assuming you want global, compile-time initialization):
None taken. The MultiVariable structure and the associated methods and functions provide enough abstraction. The command structure of the shell that this will be a part of will provide more abstraction and will protect the data from direct access.
I'm going to complete the constructors and a few other things in the morning, then it will do something.
Alternatively you can make use of one of the great features C++ has to offer, that is template metaprogramming. This is a rudimentary example, but it should (mostly) work:
inline void parse_string(const char * str, float & out) { out = atof(str); }
inline void parse_string(const char * str, int32_t & out) { out = atol(str); }
template<typename T>
void print(T & var)
{
// You can uncomment this if you want to try it in https://coliru.stacked-crooked.com/a/0664d151a0a50b13
// std::cout <<
// var.get_name() << ": " <<
// var.get_value() << std::endl;
Serial.print(var.get_name());
Serial.print(": ");
Serial.println(var.get_value());
}
class var_base
{
public:
char const * get_name() const { return name_; }
void set_name(char const * name) { memcpy(name_, name, min(static_cast<size_t>(name_size), strlen(name) + 1)); }
private:
static constexpr uint8_t name_size = 7;
char name_[name_size + 1] = {}; // + 1 for null terminator
};
template<typename T>
class basic_var :
public var_base
{
public:
T const & get_value() const { return value_; }
void set_value(T const & value) { value_ = value; }
void set_value(const char * value) { parse_string(value, value_); }
private:
T value_ = {};
};
// Specialization for a string variable
struct string_t {};
template<>
class basic_var<string_t> :
public var_base
{
public:
char const * get_value() const { return value_; }
void set_value(char const * value) { memcpy(value_, value, min(static_cast<size_t>(value_size), strlen(value) + 1)); }
private:
static constexpr uint8_t value_size = 30;
char value_[value_size + 1] = {}; // + 1 for null terminator
};
template<typename T, uint8_t quantity>
class var_pool
{
public:
basic_var<T> * find_first(char const * name)
{
for (uint8_t i = 0; i < last; ++i)
if (strcmp(pool[i].get_name(), name) == 0)
return &pool[i];
if (last < quantity) {
basic_var<T> * var = &pool[last++];
var->set_name(name);
return var;
}
return nullptr;
}
void print_all() const
{
for (uint8_t i = 0; i < last; ++i)
print(pool[i]);
}
private:
basic_var<T> pool[quantity] = {};
uint8_t last = {};
};
static var_pool<float, 15> float_vars;
static var_pool<int32_t, 25> int32_vars;
static var_pool<string_t, 5> string_vars;
void setup() {
Serial.begin(115200);
auto * var1 = int32_vars.find_first("var1");
auto * var2 = float_vars.find_first("var2");
auto * var3 = float_vars.find_first("var3");
auto * var4 = string_vars.find_first("var4");
// Set values
var1->set_value(199);
var2->set_value(1238.33);
var3->set_value("123.23"); // Or by string
var4->set_value("world");
// Print all variables
int32_vars.print_all();
float_vars.print_all();
string_vars.print_all();
}
void loop() {
}
It looks like this consumes about 10% less memory (44% vs 55% for an Arduino uno) than the code example of the previous post. You can reduce the memory usage even further if you modify it so that the variable names are stored in progmem, that is if the variable names are known at compile time of course.
I'm starting a new thread on this topic as it's diverged too much from the original question, asked and answered.
Mod said it was not cross-posting.
I'm still poking around to fix an insert issue, before I start the new thread.
Once it's up and I explain the full application, I'd value your input.