Search code examples
c++arduinoarduino-esp8266arduino-c++stdarray

Problem with access to multidimensional std::array element


I have a class like this :

    class Actuator
    {
    
    public :
        enum class Action
        {
                disable,
                turn_on,
                turn_off,
                toggle
        };
        
    private:
        /*Data member*/
        
    public :
        /*function member*/
    };

In another class i define two 2D arrays of "Action" enum class :

    class Constant_Value
    {
    private:

        static constexpr std::array<std::array<Actuator::Action, NUMBER_OF_RELAYS>, NUMBER_OF_ACTUATORS> m_actuator_relay_default_config
        {{
        {{Actuator::Action::toggle, Actuator::Action::disable, Actuator::Action::disable, Actuator::Action::disable}},
        {{Actuator::Action::disable, Actuator::Action::toggle, Actuator::Action::disable, Actuator::Action::disable}},
        {{Actuator::Action::disable, Actuator::Action::disable, Actuator::Action::toggle, Actuator::Action::disable}},
        {{Actuator::Action::disable, Actuator::Action::disable, Actuator::Action::disable, Actuator::Action::toggle}}
        }};

        static constexpr std::array<std::array<Actuator::Action, NUMBER_OF_LEDS>, NUMBER_OF_ACTUATORS> m_actuator_led_default_config
        {{
        {{Actuator::Action::toggle, Actuator::Action::disable, Actuator::Action::disable, Actuator::Action::disable}},
        {{Actuator::Action::disable, Actuator::Action::toggle, Actuator::Action::disable, Actuator::Action::disable}},
        {{Actuator::Action::disable, Actuator::Action::disable, Actuator::Action::toggle, Actuator::Action::disable}},
        {{Actuator::Action::disable, Actuator::Action::disable, Actuator::Action::disable, Actuator::Action::toggle}}
        }};
        
        struct 
        {
            std::array<std::array<Actuator::Action, NUMBER_OF_RELAYS>, NUMBER_OF_ACTUATORS> actuator_relay_config;

            std::array<std::array<Actuator::Action, NUMBER_OF_RELAYS>, NUMBER_OF_ACTUATORS> actuator_led_config;
        }m_eeprom_data;

    public:
        /*Function member*/
    };

As name of arrays indicates, the two arrays are default values so i define those as "constexpr" and struct is a editable buffer. In constructure of "Consttant_Value" class I initialize the struct buffer as following :

    Constant_Value::Constant_Value()
    {

        EEPROM.begin(sizeof(m_eeprom_data));

        //Check if the EEPROM contains valid data from another run :
        if (EEPROM.percentUsed() >= 0)
        {

            //Load data from eeprom
            EEPROM.get(0, m_eeprom_data);
        }
        else
        {

            //Prepare default date to write to EEPROM :                
            m_eeprom_data.actuator_relay_config[0] = m_actuator_relay_default_config[0];
            m_eeprom_data.actuator_relay_config[1] = m_actuator_relay_default_config[1];
            m_eeprom_data.actuator_relay_config[2] = m_actuator_relay_default_config[2];
            m_eeprom_data.actuator_relay_config[3] = m_actuator_relay_default_config[3];

            m_eeprom_data.actuator_led_config[0] = m_actuator_led_default_config[0];
            m_eeprom_data.actuator_led_config[1] = m_actuator_led_default_config[1];
            m_eeprom_data.actuator_led_config[2] = m_actuator_led_default_config[2];
            m_eeprom_data.actuator_led_config[3] = m_actuator_led_default_config[3];

            // set the EEPROM data ready for writing
            EEPROM.put(0, m_eeprom_data);

            // write the data to EEPROM
            EEPROM.commit();
        }
    }

When i compile above code following error occur :

/home/ali/.platformio/packages/toolchain-xtensa/bin/../lib/gcc/xtensa-lx106-elf/4.8.2/../../../../xtensa-lx106-elf/bin/ld: .pio/build/esp07/src/Constant_Value.cpp.o:(.text._ZN10my_program14Constant_ValueC2Ev+0x4): undefined reference to `my_program::Constant_Value::m_actuator_relay_default_config'
/home/ali/.platformio/packages/toolchain-xtensa/bin/../lib/gcc/xtensa-lx106-elf/4.8.2/../../../../xtensa-lx106-elf/bin/ld: .pio/build/esp07/src/Constant_Value.cpp.o:(.text._ZN10my_program14Constant_ValueC2Ev+0x8): undefined reference to `my_program::Constant_Value::m_actuator_led_default_config'
collect2: error: ld returned 1 exit status
*** [.pio/build/esp07/firmware.elf] Error 1

When i define a 1D temporary array as data member of Constant_Value class like :

static constexpr std::array<Actuator::Action, NUMBER_OF_RELAYS> test
{{Actuator::Action::toggle, Actuator::Action::disable, Actuator::Action::disable, Actuator::Action::disable}};

and assign it to first element of buffer like :

m_eeprom_data.actuator_relay_config[0] = test;

and comment other assignment lines it will compile successfully.


Solution

  • Your problem is related to staticvariables. The rules for static and therefore also static constexpr variables depend on the particular C++XX standard being used. If the class members are not declared static constexpr the code will work from C++11 on-wards while with it it will only work like this for C++17 and later. For versions prior to C++17 you will have to supply additional out-of-class-definitions.


    The standard for static constexpr class members has changed over the years:

    • Prior to C++17 any static class members did not have a location in memory until the variable was defined outside the class. This way one might define it at some other place, in another source file or a library. If it is used you needed to provide an out-of-class definition.

      class SomeClass {
        public:
          static int count;
      };
      
      // Out of class definition
      int SomeClass::count = 0;
      

      This also applied to constexpr class members as such class members have to be static and therefore had to be initialised.

    • In C++17 static inline class members were introduced. So somebody could define a member as inline static and provide a definition inside the class.

      class SomeClass {
        public:
          // Works since C++17
          inline static int count = 0;
      };
      

      At the same time static constexpr variables were made implicitly inline. This means any static constexpr variable would be implicitly inline static constexpr and one would have to provide a definition inside the class.


    So for C++17 compilers and onwards your code is perfectly fine while with compilers prior to C++17 you would have to provide the out-of-class definitions:

    constexpr std::array<std::array<Actuator::Action, NUMBER_OF_RELAYS>, NUMBER_OF_ACTUATORS> Constant_Value::m_actuator_relay_default_config;
    constexpr std::array<std::array<Actuator::Action, NUMBER_OF_RELAYS>, NUMBER_OF_ACTUATORS> Constant_Value::m_actuator_led_default_config;
    constexpr std::array<Actuator::Action, NUMBER_OF_RELAYS> Constant_Value::test;
    

    Test it here.

    In your case your compiler seems to be pre-C++17 and not be fully C++17 compliant (or set to C++14 or C++11): The definition of static constexpr members works with an std::array but not with an array of arrays. So something like

    constexpr std::array<std::array<Actuator::Action, NUMBER_OF_RELAYS>, NUMBER_OF_ACTUATORS> Constant_Value::m_actuator_relay_default_config;
    constexpr std::array<std::array<Actuator::Action, NUMBER_OF_RELAYS>, NUMBER_OF_ACTUATORS> Constant_Value::m_actuator_led_default_config;
    

    should be sufficient with your particular compiler. This behaviour can be observed with GCC compilers compiled with C++14 but e.g. won't compile with Clang C++14. Therefore I would adise you to do so for all three of them so your code does not break with another compiler.


    tl;dr: Prior to C++17 you have to proved out-of-class-definitions for all three class members m_actuator_relay_default_config, m_actuator_led_default_config and test. From C++17 onwards your code should compile fine.