Library Switch Case - error: 'x' is not a constant expression

I have written a library to interpret CAN messages, and I’m struggling with one small part of its intended functionality. There are a few hundred possible variables that could be sent on this network, with a few packed arbitrarily into each 8-byte message, with several dozen message IDs. The sending system has a configurable base ID, with subsequent messages offsetting the ID by increments of 1. For example, if the system is set up with a base ID of 1512, then var0 - var3 might live at the base ID of 1512, var4 to var7 at ID 1513, etc.

I would like a library user to be able to define the base ID in an .ino file without having to modify the library files, if possible. I thought that a simple way to do it would be to declare a ‘const uint32_t’ before setup() and use it as an argument to a library function call as the switch case variable, something like this:

/*~~~~~ 'can_test.ino' ~~~~*/

#include <CAN.h> //could be any of many CAN libraries, as long as it creates accurate msg.id and msg.buf[]
#include <myCANLibrary.h>

CAN Can0; //for communications
myCANLibrary myCAN; //for unpacking and storing data from Can0

CAN_message_t msg; //from generic 'CAN.h' or similar library
myCAN_message_t myCANMsg; //from my own library, used for unpacking the CAN messages, e.g. holds var0, var1, etc

const uint32_t baseID = 1512;

void setup() {
  // start the CAN bus, etc.
}

void loop() {
  if(Can0.messageAvailable()) {     //unpack the data once a new message is available, into msg.id and msg.buf[]
    myCAN.getData(baseID, msg.id, msg.buf, myCANMsg);
    Serial.print("Any variable in myCANMsg: "); Serial.println(myCanMsg.someVariable);
  }
}
/*~~~~ end of 'can_test.ino' ~~~~*/



/*~~~~ 'myCANLibrary.h' ~~~~*/

#ifndef myCANLibrary_h
#define myCANLibrary_h

#include "Arduino.h"

typedef struct myCAN_message_t {
  float someVariable = 0;
  uint16_t someOtherVariable = 0;
} myCAN_message_t;

class myCANLibrary {
  public:
    myCANLibrary();
    void getData(const uint32_t baseID, uint32_t id, const uint8_t data[8], myCAN_message_t &msg);
}

#endif
/*~~~~ end of 'myCANLibrary.h' ~~~~*/



/*~~~~ 'myCANLibrary.cpp' ~~~~*/

#include <myCANLibrary.h>
#include "Arduino.h"

myCANLibrary::myCANLibrary() {}

void myCANLibrary::getData(const uint32_t baseID, uint32_t id, const uint8_t data[8], myCAN_message_t &msg) {
  switch(id) {
    case baseID:
      msg.someVariable = ((data[0] << 8) | data[1]) / (float)1000;
      //some other variables
      break;
    case baseID + 1:
      msg.someOtherVariable = (data[0] << 8) | data[1];
      break;
    //many other cases, as 'baseID + n'
  }
}
/*~~~~ end of 'myCANLibrary.h' ~~~~*/

I get a long string of the following errors for each case in the switch:

C:\Users\Mike\Documents\Arduino\libraries\myCANLibrary\myCANLibrary.cpp: In member function 'void myCANLibrary::getData(uint32_t, uint32_t, const uint8_t*, myCAN_message_t&)':

C:\Users\Mike\Documents\Arduino\libraries\myCANLibrary\myCANLibrary.cpp:8:8: error: 'baseID' is not a constant expression

   case baseID:

        ^

C:\Users\Mike\Documents\Arduino\libraries\myCANLibrary\myCANLibrary.cpp:12:17: error: 'baseID' is not a constant expression

   case baseID + 1:

                 ^

So… what am I doing wrong? Or how can I set a baseID in the .ino file for use in the library function? I did quite a bit of searching, tried several things, but the above method was the closest to the non-library version of this, which worked (the switch was in a function in the .ino file, I believe I used ‘const uint32_t’ for the baseID).

The case needs to be evaluated at compile time. You are passing baseID trough the function parameter and therefore the compiler cannot calculate the value. The baseID as a function parameter will be passed onto the stack when the function is called. So what you want to do is semantically not allowed in C.

You could try to use a #define to specify the value at compile time.

#define basedID 1512

You are confusing the meaning of the const keyword when declaring a constant vs its use in a parameter list. The constant baseID and the parameter baseID are two different things.

When declared in the following way it defines a constant value:

const uint32_t baseID = 1512;

When declared as a parameter it indicates to the compiler that if the baseID parameter is modified in the function (method) it should be flagged as an error.

void myCANLibrary::getData(const uint32_t baseID, uint32_t id, const uint8_t data[8], myCAN_message_t &msg)

In this function the parameter baseID is a pass-by-value parameter. In other words it is a copy of the value passed to the function. The compiler does not consider this a constant because you could actually pass any value to the function. In your case you are passing the constant baseID (1512) which gets copied to the parameter baseID. Those variables (although named the same) have 2 different scopes. You could have just as easily called the getData function with another value, like 1234, in which case the value 1234 would get copied to the baseID parameter. Now you can see why the compiler does not consider it a constant.

If you remove the baseID parameter then the compiler will use the globally scoped baseID, which is a constant, and will not give you the error.

Thank you Klaus, Todd, that makes perfect sense. However, I am still struggling to get the declaration in the proper scope. If I use:

const uint32_t baseID = 1512

at the top of my .h file after removing the parameter declaration as you suggested, no problem, everything works as I’d hoped.

However, if I make the declaration at the top of the .ino file as shown below (so should be a global scope?), I get a “baseID was not declared in this scope” error within the getData() function of the .cpp file.

/*~~~~~ 'can_test.ino' ~~~~*/

#include <CAN.h> //could be any of many CAN libraries, as long as it creates accurate msg.id and msg.buf[]
#include <myCANLibrary.h>

CAN Can0; //for communications
myCANLibrary myCAN; //for unpacking and storing data from Can0

CAN_message_t msg; //from generic 'CAN.h' or similar library
myCAN_message_t myCANMsg; //from my own library, used for unpacking the CAN messages, e.g. holds var0, var1, etc

const uint32_t baseID = 1512;

void setup() {
  // start the CAN bus, etc.
}

void loop() {
  if(CAN.messageAvailable()) {     //unpack the data once a new message is available, into msg.id and msg.buf[]
    myCAN.getData(msg.id, msg.buf, myCANMsg);
    Serial.print("Any variable in myCANMsg: "); Serial.println(myCanMsg.someVariable);
  }
}
/*~~~~ end of 'can_test.ino' ~~~~*/



/*~~~~ 'myCANLibrary.h' ~~~~*/

#ifndef myCANLibrary_h
#define myCANLibrary_h

#include "Arduino.h"

typedef struct myCAN_message_t {
  float someVariable = 0;
  uint16_t someOtherVariable = 0;
} myCAN_message_t;

class myCANLibrary {
  public:
    myCANLibrary();
    void getData(uint32_t id, const uint8_t data[8], myCAN_message_t &msg);
}

#endif
/*~~~~ end of 'myCANLibrary.h' ~~~~*/



/*~~~~ 'myCANLibrary.cpp' ~~~~*/

#include <myCANLibrary.h>
#include "Arduino.h"

myCANLibrary::myCANLibrary() {}

void myCANLibrary::getData(uint32_t id, const uint8_t data[8], myCAN_message_t &msg) {
  switch(id) {
    case baseID:
      msg.someVariable = ((data[0] << 8) | data[1]) / (float)1000;
      //some other variables
      break;
    case baseID + 1:
      msg.someOtherVariable = (data[0] << 8) | data[1];
      break;
    //many other cases, as 'baseID + n'
  }
}
/*~~~~ end of 'myCANLibrary.h' ~~~~*/

I recognize the problem in my last post - defining ‘baseID’ in the .ino gave it global scope within that file.
So, I tried using an extern declaration at the top of the .cpp file just after my #includes:

#include <myCANLibrary.h>
const uint32_t baseID = 1512;
#include <myCANLibrary.h>
#include "Arduino.h"
extern const uint32_t baseID;

But I get the following error and note each time the function in the .cpp tries to use baseID (twice each, actually):

error: the value of 'baseID' is not usable in a constant expression
note: 'baseID' was not initialized with a constant expression

Why not this ?
#define baseID 1512

lesept:
Why not this ?
#define baseID 1512

It only has worked when used in the library files, not in my sketch. Going from memory, but I think it was a not defined in scope error.

mantonakakis:
Thank you Klaus, Todd, that makes perfect sense. However, I am still struggling to get the declaration in the proper scope. If I use:

const uint32_t baseID = 1512

at the top of my .h file after removing the parameter declaration as you suggested, no problem, everything works as I'd hoped.

However, if I make the declaration at the top of the .ino file as shown below (so should be a global scope?), I get a "baseID was not declared in this scope" error within the getData() function of the .cpp file.

That's because the compiler needs the constant in order to compile the getData() function.

If you want to define the value in the .ino, try this in the .cpp:

extern const uint32_t baseID;

That will tell the compiler to ask the linker to fill in the value. I think that might work but maybe not, if the compiler needs it to be a compile-time constant and not a link-time constant.

johnwasser:
If you want to define the value in the .ino, try this in the .cpp:

extern const uint32_t baseID;

That will tell the compiler to ask the linker to fill in the value. I think that might work but maybe not, if the compiler needs it to be a compile-time constant and not a link-time constant.

Yup, I tried that already and got the errors shown in post #4.

ToddL1962:
That’s because the compiler needs the constant in order to compile the getData() function.

Makes perfect sense, and after reading the “scope” post by econjack, specifically post #6 “Scope Across Two Different Source Files,” it seemed like I could give the compiler the constant by using an extern declaration in the .cpp, but alas, errors as shown in post #4 of this thread.

The reason the cases have to be constants is to allow an efficient jump-table implementation for
switch-case - its not a shorthand for a set of if/else if/ conditions. If you use small consecutive case
values you can expect the compiler to do this optimization.

Since multiple posters have suggested defining baseID at compile time as a constant which is accessible by the library function, and given fairly specific instructions on how to do it (johnwasser, but I don’t think it contradicts anyone else’s input), that’s what I’ve tried.
The following files (pared down as simply as possible to give the same errors during compilation as the full version) give the following errors.
So, what’s a good approach I can take to achieve a user-definable base ID? Get rid of the switch-case and replace with 60+ elseif{} statements? Something else?

#include <CAN.h>
#include <myCan.h>

const uint32_t baseID = 1512;

void setup() {}

void loop() {}
#ifndef myCan_h
#define myCan_h

#include "Arduino.h"

typedef struct myCan_message_t {
  float someVariable = 0;
} myCan_message_t;

class myCan {
  public:    
    myCan();
    void getData(uint32_t id, uint8_t data[8], myCan_message_t &msg);
};
#endif
#include <myCan.h>
#include "Arduino.h"

extern const uint32_t baseID;

myCan::myCan() {}

void myCan::getData(uint32_t id, uint8_t data[8], myCan_message_t &msg) {
  switch(id) {
    case baseID:
      msg.someVariable = ((data[0] << 8) | data[1]) / (float)1000;
      break;
  }
}
C:\Users\engmea\Documents\Arduino\libraries\myCan\myCan.cpp: In member function 'void myCan::getData(uint32_t, uint8_t*, myCan_message_t&)':
C:\Users\engmea\Documents\Arduino\libraries\myCan\myCan.cpp:10:10: error: the value of 'baseID' is not usable in a constant expression
     case baseID:
          ^

C:\Users\engmea\Documents\Arduino\libraries\myCan\myCan.cpp:4:23: note: 'baseID' was not initialized with a constant expression
 extern const uint32_t baseID;
                       ^

C:\Users\engmea\Documents\Arduino\libraries\myCan\myCan.cpp:10:10: error: the value of 'baseID' is not usable in a constant expression
     case baseID:
          ^

C:\Users\engmea\Documents\Arduino\libraries\myCan\myCan.cpp:4:23: note: 'baseID' was not initialized with a constant expression
 extern const uint32_t baseID;
                       ^

Alright, I did a quick comparison of two strategies to see if there was any performance impact of switch-case vs if/elseif (with my hardware). I’m using a Teensy4.0, so maybe there would be a big difference on a much slower microcontroller, but below are the results I saw. In this situation, I can’t process the full 64 groups of possible messages/frames, as my CAN broadcast hardware can only be configured to send up to 31 groups, but based on these results, I don’t expect any real significant downside of going with if/elseif in the library with a fast processor.

I used two timers for this: one which times the processing of a single group (i.e. one “myCan.getData()” call), and another that starts the timer when the baseID message comes in, and stops it when the final group comes in (31 groups in this case). Grabbed a random sample of a few dozen messages each:

  • switch-case, single message: 0-1 microseconds

  • if/elseif, single message: 0-1 microseconds

  • switch-case, all messages: 15935 microseconds (average of ~80 samples)

  • if/elseif, all messages: 15880 microseconds (average of ~80 samples)

I wouldn’t be surprised if the “all messages” results are limited by the CAN baud rate - at the speed I’m running, I think I can send around 2000 frames per second, so 31 messages should take right around 15500 microseconds just for data transfer… with either version of the function, if it takes <1us to run the data processing function, it’s still <31us of data processing total for either version…

The if/elseif version did use an extra 576 bytes of program space (no problem on the Teensy4.0, I’ve got 2MB to work with).

I’ll give it a try on a much slower processor next…

You're defining baseID in the .ino file, but trying to use it in a class definition (.h/.cpp). The class is compiled all by itself, WITHOUT the .ino file, so the compiler has no way of "seeing" the definition of baseID when the class itself is compiled.

Regards,
Ray L.

RayLivingston:
You're defining baseID in the .ino file, but trying to use it in a class definition (.h/.cpp). The class is compiled all by itself, WITHOUT the .ino file, so the compiler has no way of "seeing" the definition of baseID when the class itself is compiled.

Regards,
Ray L.

I'm not entirely sure about that... isn't that what "extern" does? Or am I missing a define vs. declare subtlety in your post? The issue is somewhat clear to me - the .cpp file ends up getting the baseID, but apparently not as a constant ("note: 'baseID' was not initialized with a constant expression").
The strategy seems to work fine when I'm not trying to use it with a switch-case. I re-wrote the library to use if/elseif statements, used "const uint32_t baseID = 1512;" in the .ino file, and used "extern const uint32_t baseID;" in the header file. Then in the .cpp I was able to use baseID without any issue, e.g. "else if(id == baseID + 62)". It compiled with no errors/notes, and functions without issue as far as I can tell.

Performance Comparison Teensy4.0 vs. Uno
My initial reason for trying the switch-case was the potential performance benefit; now I have some data to understand the magnitude of the benefit for my library, for a powerful (Teensy4.0) and not-so-powerful (Uno, er, Sparkfun BlackBoard) processor. Hopefully this table is self-explanatory enough (see previous post), based on a random sample of 65 iterations. Performance is slower on both processors with the if/else, and as expected the Uno is much slower overall, but probably not enough to matter, as the broadcast device can broadcast at 50Hz max (and the CAN baud starts to become the bottleneck when broadcasting all 31 messages at this rate anyway, so it's not a likely scenario).

I'm happy enough with this to just keep using the if/else statement, although still happy to try and learn more about getting user-defined cases to work!

extern does not solve your problem. Someone pointer out earlier that the compiler needs to know the value of constants used in case statements AT COMPILE TIME. If that is true, then extern will not help.

If you want to prove the point, setup a simple test case with a nearly empty sketch, and a nearly empty class definition, and you can prove, or disprove, this point yourself in a few minutes.

Regards,
Ray L.

If your switch statement looks like this:

  switch (id)
  {
    case baseID+0:
      break;
    case baseID+1:
      break;
  }

You could switch to this and use 'extern' for baseID:

  switch (id - baseID)
  {
    case 0:
      break;
    case 1:
      break;
  }

The expression in the 'switch' part certainly doesn't have to be a compile-time constant.

johnwasser:
If your switch statement looks like this:

  switch (id)

{
    case baseID+0:
      break;
    case baseID+1:
      break;
  }




You could switch to this and use 'extern' for baseID:


switch (id - baseID)
  {
    case 0:
      break;
    case 1:
      break;
  }




The expression in the 'switch' part certainly doesn't have to be a compile-time constant.

Ah! Clever! Will give it a try!

Finally got things to compile after using johnwasser’s idea of moving the baseID to the variable of the switch statement, which seems so obvious after the fact:

const uint32_t baseID = 1512; //in the sketch, after #include(s)
const extern uint32_t baseID; //in the header file, after #include(s)

//.cpp function:
void myCan::getData(uint32_t id, uint8_t data[8], myCan_message_t &msg) {
  switch(id - baseID) {
    case 0:
      msg.someVariable = ((data[0] << 8) | data[1]) / (float)1000;
      break;
  }
}

I’ll share my actual library here once it’s finished. I’ve not yet mentioned any details (e.g. which device is doing the CAN broadcasting) because I didn’t want anyone to end up here looking for a library like the one I’m building until I was confident it would work.

Here's the full library - I have to double check that it works as intended when I actually write my code to my device, but it compiles just fine. I had it working well before with the library-defined BASEID, so I'm pretty confident it doesn't have any issues.
MegaCAN Library