Please critique... vector of structures with std::function and virtual class methods.

Two parts... First the explanation then, in the "reply", my attempt at a solution. I would greatly appreciate critique on the code I came up with.

I am continuously working on improving my programming to make it cleaner and easier to maintain. My long term project is a data collection system with *lots* of sensors and *lots* of configurable parameters. In order to update the configuration of the systems in the field, I have a config.ini file which can be sent to the remote units over-the-air or on an SD card. So far so good. Now when I read the config.ini file, I want to do it by stepping through an array which contains the a name of the config.ini section, the key within that section, and the COMMAND (that is, function or object method) which would act on the section.key value from the file, and the function that would be the remedial action in the event the key could not be read.

So, for example: Here is how I do it now, for just one configurable parameter, without an array...

1
2
3
4
5
6
7
8
9
10
        if (ini.getValue("s_4_20_MA_1", "ACTIVE", buffer, bufferLen))
        {
            PrintSectionHas("s_4_20_MA_1", "ACTIVE", buffer);
            S_4_20_MA_1.SetActiveState(bool(std::stoi(buffer))); // 
        }
        else
        {
            PrintCouldNotRead("s_4_20_MA_1", "ACTIVE");
            // Take remedial action here.
        }


as you can see, even with that, the section and key names are used three times each.

Then we have to do this sort of thing over and over with different combinations of sections, keys, and remedial actions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
        ini.getValue("s_4_20_MA_1", "R_PER_D", buffer, bufferLen);
        S_4_20_MA_1.SetReportsPerDay(uint8_t(std::stoi(buffer)));

        ini.getValue("s_4_20_MA_1", "MATH_B4_SEND", buffer, bufferLen);
        S_4_20_MA_1.SetMath((MATH)(std::stoi(buffer)));

        // etc

        ini.getValue("s_4_20_MA_2", "ACTIVE", buffer, bufferLen);
        S_4_20_MA_2.SetActiveState(bool(std::stoi(buffer)));

        ini.getValue("s_4_20_MA_2", "S_BTWN_S", buffer, bufferLen);
        S_4_20_MA_2.SetSecondsBtwnSamples(uint16_t(std::stoi(buffer)));

        ini.getValue("s_4_20_MA_2", "R_PER_D", buffer, bufferLen);
        S_4_20_MA_2.SetReportsPerDay(uint8_t(std::stoi(buffer)));

        // etc...
        ini.getValue("S_PUL_1", "ACTIVE", buffer, bufferLen);
        S_PUL_1.SetActiveState(bool(std::stoi(buffer)));

        ini.getValue("S_PUL_1", "S_BTWN_S", buffer, bufferLen);
        S_PUL_1.SetSecondsBtwnSamples(uint16_t(std::stoi(buffer)));

// etc... etc...
// note how the type of the data passed from the config.ini changes 


So I envision a vector of structures and stepping through that...
and ultimately, a small piece of code which steps through the array...

After sleeping on it *another* night... I came up the code below. PLEASE CRITIQUE
Last edited on
Here is an attempt... using a structure which includes functions using std::function so we can support class methods and then a vector of these structures. It compiles, testing to follow...

Your comments and suggestions are welcome.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
using namespace std;
#include <bits/stdc++.h>

// declare structure
struct configLines
{
    String section;
    String key;
    std::function<void(int)> command;
    std::function<void(int)> remedial_command;
};


void loadConfigLines(void)
{

    vector<configLines> v_configLines = {
        {"heartbeat", "COLOR",
         setHeartBeatColor,
         setHeartBeatColorDefault},

        {"s_4_20_MA_1", "ACTIVE",
         S_4_20_MA_1.SetActiveState(bool()),
         S_4_20_MA_1.SetActiveState(bool(INACTIVE))}

    };
}


void processConfigIni(void)
{
    extern vector<configLines> v_configLines;
    const char *filename = "/config.ini";
    const size_t bufferLen = 80;
    char buffer[bufferLen];

// bunch of stuff happens here to check SD card and attach to .ini file on SD

    loadConfigLines();  // populate the vector of structures 
      
    // now step through the configLines
    for (int i = 0; i < v_configLines.size(); i++)
    {
        extern IniFile ini;
        if (ini.getValue(v_configLines[i, 0], v_configLines[i, 1], buffer, bufferLen));  
           // bool IniFile::getValue(const char* section, const char* key, char* buffer, size_t len, float & val) const
        {
            PrintSectionHas(v_configLines[i, 0], v_configLines[i, 1], buffer);
            v_configLines[i, 2];  // void PrintCouldNotRead(String section, String key);
        }
        else
        {
            PrintCouldNotRead(v_configLines[i, 0], v_configLines[i, 1]); // void PrintSectionHas(String section, String key, char *value);
            v_configLines[i, 3] // Take remedial action here.
        }
    }
Last edited on
It's kind of difficult to provide suggestions because it depends on what you want to achieve. But I can show you how (roughly) I would approach it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
namespace ini
{
  struct path_type
  {
    std::vector<std::string> data;

    path_type() = default;
    path_type(const std::string& section, const std::string& key) : data{section, key}
    {
    }

    bool operator==(const path_type& other) const
    {
      return data == other.data;
    }
    bool operator<(const path_type& other) const // Note: this is necessary for sorting e.g. in std::map
    {
      return data < other.data;
    }
  };

  struct file_type
  {
  ...
    bool open(const std::string& fn);
  ...
    std::optional<std::string> getValue(const path_type& path)
    {
      std::optional<std::string> result;
      if(path.data.size() != 2)
        std::cout << "Error ...";
      else
      {
        ...
      }
      return result;
    }

    std::map<path_type, std::string> getValues(const std::set<path_type>& path_set)
    {
      std::map<path_type, std::string> result;

      for(const path_type& path : path_set)
      {
        std::optional<std::string> val = getValue(path);
        if(val.has_value())
          result[path] = *val;
      }
      return result;
    }
  };
}

void processConfigIni(void)
{
    extern vector<configLines> v_configLines;
    const char *filename = "/config.ini";
    const size_t bufferLen = 80;
    char buffer[bufferLen];

// bunch of stuff happens here to check SD card and attach to .ini file on SD

    const ini::path_type paths[] = { {"heartbeat", "COLOR"}, {"s_4_20_MA_1", "ACTIVE"} };

    extern ini::file_type iniFile;
    const auto values = iniFile.getValues(std::set(std::begin(paths), std::end(paths)));

    for (const ini::path_type& path : paths)
    {
      if(values.find(path) == values.end())
      {
        PrintCouldNotRead(values.first.data[0], values.first.data[1]);
      }
    }
    for (const auto& value : values)
    {
        PrintSectionHas(value.first.data[0], value.first.data[1], value.second);
      // e.g. if(value.first == ini::path_type{"heartbeat", "COLOR"})
      // do something with value.second
    }
}
Last edited on
Coder777,
First, and most importantly, thank you.
This code is fascinating to me. It will take me a while to fully grok it and learn from it.
What I am missing, and to me, the hard part, is not so much the strings, but rather the functions/methods, which, as far as I can see are not in the code you generously provided.
So, for example, for each pair of the strings I am searching for with iniFile.getValues, I have to then run one of two functions/methods. It is adding and properly using those functions which eludes me. To be clear, when I find the section and key strings in the .ini file, I run the first function/method with the data associated with the key. If the value is not found, I run the "remedial" function/method with data included in the function as saved in the vector. DOes that explain it any better?
Yes, I'm aware that your final result is to process the data. The best way to do so depends on your needs. Here are some (but certainly not all) ways:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
namespace ini
{
...
  struct process_type
  {
      std::function<void(const std::string&)> command; // Note: the input is supposed to be string. The conversion may happen inside the function
      std::function<void()> remedial_command;

      process_type(const std::function<void(std::string)>& c, const std::function<void()>& r) : command{c}, remedial_command{r}
      {
      }
  };
...
}

struct path_process_type : ini::path_type
{
  ini::process_type proc;

  path_process_type(const std::string& section, const std::string& key, const process_type& p) : ini::path_type{section, key}, proc{p}
  {
  }
};

void setHeartBeatColor(const std::string&);
void setHeartBeatColorDefault();

void processConfigIni(void)
{
  using namespace std::placeholders;    // adds visibility of _1, _2, _3,...

// bunch of stuff happens here to check SD card and attach to .ini file on SD

    const auto lambda1 = [](const std::string& s)
    {
      S_4_20_MA_1.SetActiveState(bool());
    };
    const auto lambda2 = []()
    {
      S_4_20_MA_1.SetActiveState(bool(INACTIVE))
    };

    const path_process_type paths[] = { {"heartbeat", "COLOR", {std::bind (setHeartBeatColor,_1), setHeartBeatColorDefault}}
    , {"s_4_20_MA_1", "ACTIVE", {lambda1, lambda2}} };

    extern ini::file_type iniFile;
    const auto values = iniFile.getValues(std::set(std::begin(paths), std::end(paths)));

    for (const path_process_type& p : paths)
    {
      if(values.find(p) != values.end())
      {
        PrintCouldNotRead(values.first.data[0], values.first.data[1]);
        p.proc.remedial_command();
      }
      else
      {
        PrintSectionHas(value.first.data[0], value.first.data[1], value.second);
        p.proc.command(value.second);
      }
    }
}
For bind() see:

https://cplusplus.com/reference/functional/bind/

For lambda functions see:

https://en.cppreference.com/w/cpp/language/lambda

Note that it is not tested!
Wow Coder777. Thank you. Okay... so a lamda has to be created for each of the class methods and I will use bind for "normal" functions. Got it.

So, no declaration of a std::vector needed? Just build the vector? Vector is implied and understood by the compiler?
 
 const path_process_type paths[] = { {"heartbeat", "COLOR", {std...


The stuff in the namespace ini section and the following struct declaration, I think I get it when I read it but I certainly don't get it to the point where I could write it myself. Any suggestions for learning that part?

Should this line:
 
struct path_process_type : ini::path_type

read
 
struct path_process_type : ini::process_type

? I see no "path_type" as a potential parent.

Thanks again. I will work with this until I grok it.

So, no declaration of a std::vector needed? Just build the vector? Vector is implied and understood by the compiler?

That's a C-style array, not a vector.
Last edited on
a lamda has to be created for each of the class methods and I will use bind for "normal" functions.
Not necessarily. When you have member function you can use bind() as well. It just needs to be the right signature:
1
2
3
4
5
6
7
8
9
struct x
{
  void FromIni(const std::string& s);
};

...
x someObject;
...
const path_process_type paths[] = { {"x", "y", {std::bind (&x::FromIni, &someObject,_1), ...


So, no declaration of a std::vector needed? Just build the vector? Vector is implied and understood by the compiler?
As @MikeyBoy pointed out it is not a std::vector. In this case the additional features are not used.

? I see no "path_type" as a potential parent.
For brevity I omitted path_type in my last post. See the previous. And yes the base must be path_type because iniFile.getValues(...) expects path_type.
Registered users can post here. Sign in or register to post.