c ++ - Bedre måde at kortlægge strengfelter til variabler

Indlæg af Hanne Mølgaard Plasc

Problem



Jeg har nogle globale variabler, som vil blive tildelt en værdi, når konfigurationsfilen er læst.


bool bar1;
int bar2;
string bar3;


Jeg læser konfigurationsfilen, der ligner nedenfor:


foo1 = 12
foo2 = 0
foo3 = 1
...
void func()
{
   //read file into a std::map mp
   for(auto i:mp)
   {
      if(i.first=="foo1")
         bar1 = i.second;
      else if(i.first=="foo2")
         bar2 = i.second;
      else if(i.first=="foo3")
         bar3 = i.second;
       .....
   }
} 


Jeg har mange sådanne variable at initialisere fra en fil. Er der en bedre måde at gøre dette på, fordi det vil opblussen min funktion.


PS: Jeg sidder stadig fast med C ++ 03.

Bedste reference


I min kommentar uddybede jeg lidt på ideen om Jabberwocky at bruge en std::map.


Faktisk gør vi lignende ting i vores S/W for konfiguration og lignende ting. Den eneste forskel – vi bruger ikke en std::map til dette, men en foruddefineret rækkefølge. (Jeg kunne ikke godt lide tanken om, at der skal gøres noget på run-time, som faktisk aldrig ændres efter kompilering.) At demonstrere konceptet Jeg lavede en lille MCVE:


#include <iostream>
#include <cassert>
#include <cstring>
#include <algorithm>
#include <map>

int main()
{
  // variables
  int bar1 = 0, bar2 = 0, bar3 = 0;
  // symbol table
  const struct Entry { const char *key; int *pVar; } table[] = {
    { "foo1", &bar1 },
    { "foo2", &bar2 },
    { "foo3", &bar3 }
  };
  const size\_t nTable = sizeof table / sizeof *table;
  // check that table has correct order
  assert([&]()
    {
      for (size\_t i = 1; i < nTable; ++i) {
        if (strcmp(table[i - 1].key, table[i].key) >= 0) return false;
      }
      return true;
    }());
  // use table in tests
  std::pair<const char*, int> mp[] = {
    { "foo1", 123 },
    { "foo2", 234 },
    { "foo3", 345 },
    { "wrong", 666 }
  };
  // evaluate mp of OP
  for (auto i : mp) {
    const Entry e = { i.first, 0 };
    const auto iter
      = std::lower\_bound(std::begin(table), std::end(table), e,
        [](const Entry &e1, const Entry &e2) { return strcmp(e1.key, e2.key) < 0; });
    if (iter != std::end(table) && strcmp(iter->key, i.first) == 0) *iter->pVar = i.second;
    else std::cerr << "Unknown var '" << i.first << "'!
";
  }
  // print result
  std::cout
    << "bar1: " << bar1 << '
'
    << "bar2: " << bar2 << '
'
    << "bar3: " << bar3 << '
';
  // done
  return 0;
}


Produktion:


Unknown var 'wrong'!
bar1: 123
bar2: 234
bar3: 345


Live demo på coliru [20]


Den væsentlige del er struct Entry, som grupperer navnet på en mulighed med adressen til den tilsvarende variabel. Dette kunne bruges til at gemme par navne og variable adresser i en std::map.


Jeg brugte i stedet en forud sorteret array. (Sortering af tasterne manuelt i programmering er ikke så svært. I tilfælde af ulykker vil assert() advare.)


I vores produktive S/W anvendte vi ikke adresser på variabler, men metodepeger til setter funktioner, da destinationsvariablerne har forskellige typer og værdierne (giver som streng) er underlagt en resp. Parsing. Disse metodepegere er dog kompileringstid opløselige → hele tabellen kan være static. Derfor er indsatsen for at opbygge bordet for hvert funktionsopkald forhindret. I denne demo adresserer tabellen butikkerne til lokale variabler. et static bord kunne være en dårlig ide (og jeg prøvede det heller ikke).





Efter anmodning her en anden demonstration ved hjælp af metoden peger til setter metoder:


#include <iostream>
#include <cassert>
#include <cstring>
#include <string>
#include <algorithm>

class Object {
  private:
    // some member variables:
    int var1, var2;
    std::string var3;
    double var4;

  public:
    Object(): var1(), var2(), var4() { }

    friend std::ostream& operator<<(std::ostream &out, const Object &obj);

    // the setter methods
    void setVar1(const char *value) { var1 = atoi(value); }
    void setVar2(const char *value) { var2 = atoi(value); }
    void setVar3(const char *value) { var3 = value; }
    void setVar4(const char *value) { var4 = strtod(value, nullptr); }

    // the config method to set value by text    
    void config(const char *key, const char *value)
    {
      // symbol table
      static const struct Entry {
        const char *key; // the symbol
        void (Object::*set)(const char*); // the corresponding setter method
      } table[] = {
        { "var1", &Object::setVar1 },
        { "var2", &Object::setVar2 },
        { "var3", &Object::setVar3 },
        { "var4", &Object::setVar4 }
      };
      enum { nTable = sizeof table / sizeof *table };
      // check that table has correct order (paranoid - debug only code)
      assert([&]()
        {
          for (size\_t i = 1; i < nTable; ++i) {
            if (strcmp(table[i - 1].key, table[i].key) >= 0) return false;
          }
          return true;
        }());
      // find setter by key
      const Entry e = { key, nullptr };
      const auto iter
        = std::lower\_bound(std::begin(table), std::end(table), e,
          [](const Entry &e1, const Entry &e2) { return strcmp(e1.key, e2.key) < 0; });
      if (iter != std::end(table) && strcmp(iter->key, key) == 0) {
        (this->*iter->set)(value);
      } else std::cerr << "Unknown var '" << key << "'!
";
    }
};

std::ostream& operator<<(std::ostream &out, const Object &obj)
{
  return out
    << "var1: " << obj.var1 << ", var2: " << obj.var2
    << ", var3: '" << obj.var3 << "', var4: " << obj.var4;
}

int main()
{
  Object obj;
  // print obj before config:
  std::cout << "obj: " << obj << '
';
  // configure obj
  std::pair<const char*, const char*> config[] = {
    { "var1", "123" },
    { "var2", "456" },
    { "var3", "text" },
    { "var4", "1.23" },
    { "evil", "666" }
  };
  for (const auto& entry : config) {
    obj.config(entry.first, entry.second);
  }
  // print obj after config:
  std::cout << "obj: " << obj << '
';
  // done
  return 0;
}


Produktion:


obj: var1: 0, var2: 0, var3: '', var4: 0
Unknown var 'evil'!
obj: var1: 123, var2: 456, var3: 'text', var4: 1.23


Indholdet af table (i Object::config()) er static const og vil blive bygget på kompileringstidspunktet (og forhåbentlig 'brændt' i binæret). Derfor har de mange opkald af Object::config() den eneste indsats for binær søgning af matchende key og kalder setteren i tilfælde af succes.


En væsentlig forudsætning er, at alle setter metoder har samme signatur. Ellers ville det ikke være muligt at gemme dem i et array, da de alle skal være kompatible med metodepegerelementet i arrayet.


Live demo på coliru [21]