Access multiple structs of differing sizes, like an array?

Hello,

I am wondering if there is a convenient way to access multiple structs (of different size), as if they were part of a larger array?

So, let’s say firstly, that I declare an array of long variable’s as follows;

long arrayoflong[2] = {1234,5678}

And I do something simple, like calculating the size of each of the individual long variable's (obviously pointless as they're all the same size, but just an example).

both long variable's are represented by the indexNumber 0 and 1 respectively, and the example checks the size for both.

Example 1:

long arrayoflong[2] = {1234,5678};
byte indexNumber = 0;

void setup() {
  // put your setup code here, to run once:
Serial.begin(9600);
}

void loop() {
  // put your main code here, to run repeatedly:

int Size = sizeof(arrayoflong[indexNumber]);

Serial.println(Size);

//goto next variable
  if (indexNumber == 1) {
    indexNumber = 0;
  }
  else {
    indexNumber++;
  }

delay(2000);

}

However, If I instead used 2 structs of differing size, the easiest way I know of to access the 2 would be using a switch case statement, as follows;

So this is a repeat of the example above, but I'm calculating the sizes of both my structs.

txdata_0 and txdata_1 are represented as indexNumber 0 and 1 respectively.

Example 2:

byte indexNumber = 0;

struct TxStruct_0 {
  long distance;
  long Time;
  long Day;
  char Name[6];
};

TxStruct_0 txData_0 = {1000, 2000, 3000, "hello"};

struct TxStruct_1 {
  long pressure;
  long humidity;
  long flowRate;
  long weight;
  char Name[6];
};

TxStruct_1 txData_1 = {4000, 5000, 6000, 7000, "world"};


void setup() {
  // put your setup code here, to run once:
Serial.begin(9600);
}

void loop() {
  // put your main code here, to run repeatedly:
int Size;
  
  switch (indexNumber) {
    case 0:
      Size = sizeof(txData_0);
      break;

    case 1:
      Size = sizeof(txData_1);
      break;
  }

Serial.println(Size);

//go to next struct
  if (indexNumber == 1) {
    indexNumber = 0;
  }
  else {
    indexNumber++;
  }

delay(2000);

}

Is there any way I could have both of these structs (of differing size and variables) within some kind of unified package, where they can be accessed similar to example 1 above, which would allow me to omit the switch...case statement?

So I could perhaps have a function like this, which gets the size of txData_0 at indexNumber 0 and the size of txData_1 at indexNumber 1?

int Size = sizeof(arrayofStructs[indexNumber]);

The closest example I've managed to find is the accessing of multiple int arrays (of differing size) using an additional struct: Multi-dimensional arrays with different number of elements - #2 by sterretje

Could something similar be used for multiple structs? Or is this just not how these things work?

thanks a lot.

In C++ only you and the compiler know what's inside a struct.

At run time it's just a blob of bytes you can't introspect programmatically to extract the different members by type (there might even been padding and a different layout than what you described in the struct if the compiler chose so)

You would need some other information that would describe the struct's content (that's often used for serialisation and deserialisation - see for example Boost's serialization or Google's Protocol buffers. There is the Nanopb - Protocol Buffers for Embedded Systems library if you are interested in exploring this and a tutorial on ESP8266)

You could make the two structs children of the same parent class P and create an array of pointers of type P*.

struct P {
  int id;
};

struct C1 : public P {
  int x;
};

struct C2 : public P {
  int y;
  int z;
};

C1 c1;
C2 c2;
P* p[2] = {&c1, &c2};

void setup() {
  Serial.begin(9600);

  c1.id = 1;
  c2.id = 2;
  for (size_t i = 0; i < 2; i++) {
    Serial.println(p[i]->id);
  }
}

void loop() {}

that does not let you access programmatically the size of the individual child's member variables (and I don't think the standard Arduino compile process gives you access to the typeid operator because of the -fno-rtti flag)

Ah, I did not read the code, only the question. Never mind then.

[edit]

What about this then?

struct P {
  size_t const size;
  P(size_t const size) : size(size) {}
};

struct C1 : public P {
  int x;
  C1(int const x) : P(sizeof(C1)), x(x) {}
};

struct C2 : public P {
  int y;
  int z;
  C2(int const y, int const z) : P(sizeof(C2)), y(y), z(z) {}
};

C1 c1(1);
C2 c2(2, 3);
P* p[2] = {&c1, &c2};

void setup() {
  Serial.begin(9600);

  for (size_t i = 0; i < 2; i++) {
    Serial.println(p[i]->size);
  }
}

void loop() {}
1 Like

Yes, lemmie get on PC as i’m on mobile atm

struct P 
{
	int id;
	virtual size_t GetSizeOf() const
	{
		return sizeof(*this);
	}
	virtual ~P() = default;
};

struct C1 : P 
{
	int x;
	size_t GetSizeOf() const override
	{
		return sizeof(*this);
	}
};

struct C2 : P 
{
	int y;
	int z;
	size_t GetSizeOf() const override
	{
		return sizeof(*this);
	}
};

C1 c1;
C2 c2;
P* p[2] = { &c1, &c2 };

void setup() 
{
	Serial.begin(9600);

	c1.id = 1;
	c2.id = 2;
	for (size_t i = 0; i < 2; i++) 
	{
		Serial.println(p[i]->GetSizeOf());
	}
}

void loop() 
{
}
1 Like

Thanks @jfjlaros and @killzone_kid!!! Can there be a mixture of different types of variable's within both child structs, rather than just int's, ?

so for example, 2 longs, an int and a char array in one of the struct's?

Sure, but keep in mind struct can get padded and the size of it can be > sum of sizes of members

Just to clarify, do you need the size of the struct or dynamically iterate though each member variable for a random struct and find out their size?

Finding the size of the struct is easy, getting a list of all the attributes so that you can check out their size is not (requires some work, esp. with inheritance)

Could add a serialisation method to each struct and manually set rules and sizes

Yep, suggested in post #2

I really should've mentioned this at the beginning, but my whole goal is to have 2 or more of the structs sent through through a UART port (sent separately at different intervals) and retrieved and parsed by a different Arduino.

This is all part of a bidirectional RS485 network, and the idea of having 2 or more different sized structs accessed through an array is not essential, but would be quite convenient.

As you can see, each of the structs is sent at different intervals, along with an ID byte at the beginning, so that the receiver knows which struct is being sent through.

My goal was to omit that switch...case statement on the sender side, and have each struct accessed through an array, and sent like this;

sendSerial.write(startMarker);
sendSerial.write(ID_byte);
sendSerial.write((byte*) &arrayOfStructs[ID_byte], sizeof(arrayOfStructs[ID_byte])); 
sendSerial.write(endMarker);

Is such a thing doable with the 1 parent and multiple child struct setup?

Here is the transmitting arduino:

#include <SoftwareSerial.h>
SoftwareSerial sendSerial(2,3); // Rx and Tx

byte sendBuff[60];
byte sendSize = 0;

byte startMarker = 0x3C;
byte endMarker = 0x3E;
byte ID_byte = 0;

//structs;

struct TxStructONE {
    long distance;         
    long Time;              
    long Day;    
    char Name[6];      
                          
};

TxStructONE txData = {1000, 2000, 3000,"hello"};

struct TxStructTWO {
    long pressure;        
    long humidity;          
    long flowRate;    
    long weight;
    char Name[6];
                           
};

TxStructTWO txData2 = {4000, 5000, 6000,7000,"world"};

void setup() {
  // put your setup code here, to run once:
Serial.begin(9600);
sendSerial.begin(9600);

}

void loop() {

sendSerial.write(startMarker);
sendSerial.write(ID_byte);

switch(ID_byte){
    case 0:
    
sendSerial.write((byte*) &txData, sizeof(txData)); 
    break;
  
    case 1:
sendSerial.write((byte*) &txData2, sizeof(txData2)); 
    break;     
}
sendSerial.write(endMarker);


//print what packet we sent
Serial.print("sent packet #");
Serial.println(ID_byte);

//goto next packet, or return to beginning
if(ID_byte == 1){
ID_byte = 0;
}
else{
ID_byte++;   
}

delay(2000);

}

And the receiving Arduino:

// Example 6 - Receiving binary data
#include <SoftwareSerial.h>
SoftwareSerial sendSerial(2,3); // Rx and Tx

const byte numBytes = 60;
byte receivedBytes[numBytes];
byte numReceived = 0;

boolean newData = false;


struct RxStructONE {
  
    long distance;         
    long Time;              
    long Day;    
    char Name[6];  
        
};

RxStructONE rxData;

struct RxStructTWO {

    long pressure;        
    long humidity;          
    long flowRate;    
    long weight;
    char Name[6]; 
              
};

RxStructTWO rxData2;


byte sendSize = 0;
byte ID_byte = 0;
byte counter = 0;

void setup() {
    Serial.begin(9600);
    Serial.println("<Arduino is ready>");
    sendSerial.begin(9600);
}

void loop() {
    recvBytesWithStartEndMarkers();
    showNewData();
}

void recvBytesWithStartEndMarkers() {
    static boolean recvInProgress = false;
    static byte ndx = 0;
    byte startMarker = 0x3C;
    byte endMarker = 0x3E;
    byte rb;
   

    while (sendSerial.available() > 0 && newData == false) {
        rb = sendSerial.read();

        if (recvInProgress == true) {
            if (rb != endMarker) {
                receivedBytes[ndx] = rb;
                ndx++;
                if (ndx >= numBytes) {
                    ndx = numBytes - 1;
                }
            }
            else {
                receivedBytes[ndx] = '\0'; // terminate the string
                recvInProgress = false;
                numReceived = ndx;  // save the number for use when printing
                ndx = 0;
                newData = true;
            }
        }

        else if (rb == startMarker) {
            recvInProgress = true;
        }
    }
}



void showNewData() {
    if (newData == true) {
      
counter = 1;
ID_byte = receivedBytes[0]; 
Serial.println("/////////////////////////////////////");
Serial.println(" ");
Serial.print("Packet number  ");
Serial.println(ID_byte);
Serial.println(" ");

switch(ID_byte){
    case 0:
    Serial.println("STRUCT 1");
    Serial.println(" ");
    readByteArray(rxData);
    Serial.print("distance ");
    Serial.println(rxData.distance);
    Serial.print("Time ");
    Serial.println(rxData.Time);
    Serial.print("Day ");
    Serial.println(rxData.Day);
    Serial.print("Name ");
    Serial.println(rxData.Name);
    break;  
    
    case 1:
    Serial.println("STRUCT 2");
    Serial.println(" ");
    
    readByteArray(rxData2);
    Serial.print("pressure ");
    Serial.println(rxData2.pressure);
    Serial.print("humidity ");
    Serial.println(rxData2.humidity);
    Serial.print("flowRate ");
    Serial.println(rxData2.flowRate);
    Serial.print("weight ");
    Serial.println(rxData2.weight);
    Serial.print("Name ");
    Serial.println(rxData2.Name);    
    break;      
}
        newData = false;
Serial.println("/////////////////////////////////////");    
    }
 
}



template<typename Value> void readByteArray(Value &value) { //unpack the byte array into the variable

    byte * p = (byte*) &value;
    unsigned int i;
    for (i = 0; i < sizeof value; i++){
      *p++ = receivedBytes[counter];
      counter++;
    }
    return i; 
}

one approach is a union. but again, to know the format of the data requires some identifier in the data

you could have various structs of various sizes and format. if the very first element (possibly a "packed" struct) is an identifier. a generic ptr to a struct contain just the identifier which is used to read the identifier. based on the identifier, the ptr is recast for the appropriate struct, as well as invoking the appropriate sub-function to process it.

yes, this doesn't avoid the "switch" thing you suggest and requests separate processing which i think is unavoidable.

this approach might be used when passing messages of various formats knowing just the length of the data and using the message ID (i.e. identifier).

same for data possible stored in file. see TLV

1 Like

if your struct is trivial / flat (ie it does not contain a pointer to data stored elsewhere outside the structure itself) then you can indeed directly dump the bytes of the struct over the Serial line.

An array in C++ holds element of the same type by definition of an array. C++ doesn't technically support hetrogenious containers in the standard library (STL) but other libraries such as Boost would ( boost::variant and boost::any)

you "technically could" cheat a bit and keep pointers to the struct elements inside an array as void pointers (then all elements have the same type) and the associated sizes in another array that you'll use to dump the data.

Something like this should compile and do what you want. it's kinda OK because you never change the void pointer back to its original type and just use that to walk through the bytes in memory.

struct __attribute__((packed)) A {
  char x;
  char y;
};

struct __attribute__((packed)) B {
  char x;
  char y;
  char text1[10 + 1];
};


struct __attribute__((packed)) C {
  char x;
  char y;
  char text1[10 + 1];
  char text2[3 + 1];
};

A a = {'A', 'a'};
B b = {'B', 'b', "xxxxxxxxxx"};
C c = {'C', 'c', "zzzzzzzzzz", "yyy"};

void * myArray[] = {&a, &b, &c};
size_t myArraySize[] = {sizeof a, sizeof b, sizeof c};
size_t count = sizeof myArray / sizeof * myArray;

void setup() {
  Serial.begin(115200);
  for (size_t i = 0; i < count; i++) {
    Serial.print(i);
    Serial.print(F(":\t"));
    Serial.write((byte*) myArray[i], myArraySize[i]);
    Serial.println();
  }
}

void loop() {}

it's not really nice though....

Note that I used char for the struct members so that it will print to the console as ascii text and be readable. Of course if you had other types you would get a binary dump of the bytes (little endian for the integers). I also filled in all the arrays with characters to avoid having the null char padding done by the compiler which would not show in the Serial monitor.

__attribute__((packed)) is a GCC extension that instruct the compiler to not add any padding.

1 Like

You can probably do it with some of the tips outlined above, but it'll be dirty, fragile and hard to debug.

I'd be particularly concerned about getting it going and then in a month or two, wanting to tweak something or add another Arduino to the network and finding that a newer version of the IDE has different compiler flags, or the shiny new Teensy you added compiled using different rules and suddenly it's broken but you won't know why.

It's more bytes on the wire, but if you can, I'd suggest sending the data in human readable form so it's easier to debug.

Could make a method within struct instead to get size

Serial.write(reinterpret_cast<byte *>(myArray[i]), myArray[i]->SelfSize());

Have to mention struct can have POD types only or serialisation will need to be done differently

1 Like

boggles my mind.
wouldn't any reviewer, as well as a newbie appreciate making this more readable

Stroustrup in his 3rd edition warned programmers to only use the features needed and not necessarily a feature just because it's there

instead of using size to determine the format of the struct, why not simply have unique IDs (as mentioned)?

This is as transparent as it gets, not sure what is confusing. Also keeping and maintaining another array just for sizes is not a good design

That's looking absolutely perfect. Thanks so much!

Should also be easy to have the receiver Arduino transfer the data from a temporary byte buffer, back into the appropriate struct.

Maybe not the most elegant thing, but should be very handy!

Is that "padding" issue to do with the fact that some MCU's will will add more bytes to the struct, even if it doesn't match the amount of variable's contained inside? I think this thread mentions this issue; Size of struct differences?