Creating a default copy of a complex-ish config onject

I don't know C/C++ very well and I am getting seriously flogged here!
I have a config object that is a little complicated:

Config getDefaultConfig() {
  Config config;
  config.checksum = 0;
  config.mustRunInitialSetup = true;
  strncpy(config.deviceName, "YOUR FEEDER", sizeof(config.deviceName));
  config.deviceName[sizeof(config.deviceName) - 1] = '\0';
  config.soilDryValue = 0;
  config.soilLittleMoistValue = 0;
  config.soilMoistValue = 0;
  config.soilVeryMoistValue = 0;
  config.trayNeedsEmptying = false;
  config.moistSensorCalibrationWet = 0;
  config.emptyTraySensorCalibrationWet = 0;
  config.emptyTraySensorCalibrationWellWet = 0;

  // Set default checksum
  config.checksum = calculateConfigChecksum(config);

  // Define default actions
  config.actions[0] = Action {
    "EMPTY TRAY",
    { WateringStyle::TOP_AND_BOTTOM },
    { },
    { Op::WATER_OUT },
    { Conditions::TRAY_EMPTY, Conditions::SOIL_IGNORED, Conditions::NO_LOGIC },
    true
  };
  // ...for 12 more actions objects
 // Define default active actions
  for (int i = 0; i < 4; ++i) {
    config.activeActions[i].actionIndex = i;
    config.activeActions[i].everyNFeeds = 0;
    config.activeActions[i].maxNTimesPerHour = 0;
  }
  return config;
}

This is what the config object looks like:

typedef struct {
  enum { P1, S1 } what;
} WaterSources;

typedef struct {
  enum { NO_WATER, WATER_IN, WATER_OUT, WATER_IN_AND_OUT} what;
} Op;

typedef struct {
  enum { NEITHER_TOP_OR_BOTTOM, TOP_AND_BOTTOM, ONLY_TOP, ONLY_BOTTOM } style;
} WateringStyle;

typedef struct {
  enum { TRAY_IGNORED, TRAY_EMPTY, TRAY_AT_MOST_WET, TRAY_AT_LEAST_WET, TRAY_AT_MOST_HALF_FULL, TRAY_AT_LEAST_HALF_FULL, TRAY_FULL } tray;
  enum { SOIL_IGNORED, SOIL_DRY, SOIL_AT_MOST_DAMP, SOIL_AT_LEAST_DAMP, SOIL_AT_MOST_WET, SOIL_AT_LEAST_WET, SOIL_VERY_WET } soil;
  enum { NO_LOGIC, TRAY_OR_SOIL, TRAY_AND_SOIL } logic;
} Conditions;

typedef struct {
  char name[15];
  WateringStyle wateringStyle;
  Conditions triggerConditions;
  Op op;
  Conditions endConditions;
  bool active;
} Action; 

typedef struct {
  int actionIndex;
  int everyNFeeds; // 0 for every time
  int maxNTimesPerHour; // 0 for every time
} ActiveAction;

typedef struct {
    uint8_t checksum;
    bool mustRunInitialSetup;
    char deviceName[20];
    int soilDryValue;
    int soilLittleMoistValue;
    int soilMoistValue;
    int soilVeryMoistValue;
    
    bool trayNeedsEmptying;

    int moistSensorCalibrationWet;
    int emptyTraySensorCalibrationWet;
    int emptyTraySensorCalibrationWellWet;
    Action actions[12];
    ActiveAction activeActions[4];
} Config;

In my setup() I have this:

Config config; // Global file-wide variable. I need it in setup() and loop()
void setup() {
  // ...
  if (!loadConfig(config)) { // This is always true BTW, I don't even know why (yet)
    config = getDefaultConfig(); // in THEORY the global variable should be copied over
    alert("EEPROM Error!");
  }

Since Config getDefaultConfig() returns a Config object, I assumed that writing this: config = getDefaultConfig(); would overwrite the global variable config with whatever is returned.

However, when I do this, there is clearly corruption -- screen blinks, in a weird death loop. Variations in the function will give you variations in the weirdness. All of them smelling like buffer overflows.

I kind of "know" how to make it work. Allocating config and then overwriting it is the only way I know NOT to cause buffer overflows.

Having this in my loop:

Config config; // Global file-wide variable.
void setup() {
  // ...
  if (!loadConfig(config)) {
    setAsDefaultConfig(config);
    alert("EEPROM Error!");
  }

And changing the function into:

void setAsDefaultConfig(Config &config) {

It works. Well, this is the only way I know to make sure there can't be any memory funkiness: I allocate config, and then I pass that config variable as is to the function, and then I make sure it's overwritten with the "clean" stuff.

What really bugs me is... WHY doesn't the other way work, and -- worse -- it leads to horrible memory overwriting?

What am I doing wrong?

Does it? Or does it return a pointer to an object (is it actually an object...or a struct)?

Well, it defines an object:

Config getDefaultConfig() {
   Config config;

...and it returns it:

return config;

So... is it returning a pointer...?!?

It's a custom type

What type arduino are you using? Config is a struct that takes 445 bytes of storage, having to allocate that much temporary storage when returning it from a function is possibly running out of memory.

Passing a reference to config definitely takes less memory, otherwise the setAsDefaultConfig() function needs the 445 bytes of memory to create the local struct.

1 Like

Holy crap you must be right. I am using a Mini, 2k of RAM!

You are on the money -- thank you. That was it. It makes a copy, but there is no space for the copy.
So... no buffer overflow protection of ANY kind on these little things? I am so not used to this... I am completely out of my depth!

this is a correct assumption

do you have enough memory ? what Arduino are you using?

EDIT: I had not seen the post from @david_2018 when I wrote my answer. weird.

Nope straight to stack overflow :slight_smile:

You can save a bit of memory if you tell the compiler to use a single byte for the enums, instead of defaulting to a two-byte integer.

typedef struct {
  enum : uint8_t { P1, S1 } what;
} WaterSources;

typedef struct {
  enum : uint8_t { NO_WATER, WATER_IN, WATER_OUT, WATER_IN_AND_OUT} what;
} Op;

typedef struct {
  enum : uint8_t { NEITHER_TOP_OR_BOTTOM, TOP_AND_BOTTOM, ONLY_TOP, ONLY_BOTTOM } style;
} WateringStyle;

typedef struct {
  enum : uint8_t { TRAY_IGNORED, TRAY_EMPTY, TRAY_AT_MOST_WET, TRAY_AT_LEAST_WET, TRAY_AT_MOST_HALF_FULL, TRAY_AT_LEAST_HALF_FULL, TRAY_FULL } tray;
  enum : uint8_t { SOIL_IGNORED, SOIL_DRY, SOIL_AT_MOST_DAMP, SOIL_AT_LEAST_DAMP, SOIL_AT_MOST_WET, SOIL_AT_LEAST_WET, SOIL_VERY_WET } soil;
  enum : uint8_t { NO_LOGIC, TRAY_OR_SOIL, TRAY_AND_SOIL } logic;
} Conditions;

No memory overflow protection, and no protection again writing outside the bounds of an array, although occasionally the compiler can detect that and give you a warning. There is also no garbage collection on the heap, which can lead to problems when using String variables.

The forum has been acting up the last few weeks, constantly loading the page and not showing up responses until you hit reload.

1 Like

yep, noticed that

No, a struct. Not an object. That's why you're running into the memory problem. Had I been right about my suspicion that it was passing back a reference, it wouldn't have happened. And if you had been passing back an actual object, it would in fact have been passed back as a reference.

Object != struct.

the only difference is that in the struct the members are public by default and private in a Class...

so with just a bit of abuse you can talk about an Object (as defined as a instance of a Class) when you talk about a struct instance (a variable of the type of the Structure)...

You can pass Structs by reference in the same way you pass an Instance by reference.

not sure what you mean by that. You have to tell the compiler you want a reference.

But it is a rather unusual way of doing things. Why not just have the constructor create the "Default Config" in the first place?

Yes - there are better ways of doing that like also passing the config instance by reference

there are Arduino functions returning a local instance by copy, so not unheard of - cf .readString() for example

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.