I would like to recommend a more general solution for the problem of interdependent, but scattered data in a program.
It's using a single preprocessor macro file which contains all information in a single place, in the form of a list of preprocessor function macros. The file is included wherever the information is needed to define data structures or types like enums.
In this example I use a list of paper size defintions; this served me nicely in real life when I wrote a PCL6 parser. In PCL6 each paper size is indeed a numerical token. Paper sizes have quite a few associated attributes, as well as a human readable name. Frequently the need occurs to map bidirectionally between name and token value and look up associated information. This leads to several data structures with redundant information plus a matching enum definition. It's easy to miss an update in one of them when adding a new paper type (in reality there are many dozen).
The trick is to define an entry in the macro file to a language construct which is suitable in the given place. Note how the language was carefully designed to allow trailing commas e.g. at the end of an enum definition or initializer lists. This is the use case for that.
We start with the file containing the macros which hold the information associated with a paper size. In reality there are of course many more, and more attributes.
//////////////////////////////////////////////////////
// papermacros.h
//////////////////////////////////////////////////////
// Has all information about paper sizes in one place.
// Note the function syntax. It's essential.
// token, name, height, width
PAPERSIZE_MACRO(5, A5, 200, 150)
PAPERSIZE_MACRO(4, A4, 300, 200)
PAPERSIZE_MACRO(3, A3, 400, 300)
Then the paper class and enum. The macros are used to build an enum type
of paper tokens which always contains all entries in the
macro file. The enum element names are constructed with the preprocessor concatenation operator, names are constructed using the stringize operator. (We cannot have strings in the macro header right away because we want to use the name also as a base for the enum identifier -- there is no "unstringize" operator.)
//////////////////////////////////////////////////////
// papers.h
//////////////////////////////////////////////////////
#include <string>
#include <map>
#include <sstream>
#undef PAPERSIZE_MACRO
#define PAPERSIZE_MACRO(token, name, height, width) \
e_##name = token,
enum PaperSizeE {
e_INVALID, // for default ctor
# undef PAPERSIZE_MACRO
# define PAPERSIZE_MACRO(token, name, height, width) \
e_##name = token,
// this included file expands to a series of enum definitions which
// make sure that each enum element is named
// like the paper name, with a prefix e_
# include "papermacros.h"
e_END // if you want. Note, however, that it has the (arbitrary)
// value of the last "real" enum plus 1.
#undef PAPERSIZE_MACRO
};
class PaperT
{
public:
PaperSizeE token;
int height;
int width;
std::string name;
PaperT(PaperSizeE t, std::string n, int h, int w)
:token(t), name(n), height(h), width(w)
{ }
// Funny, needed by map resp. std::pair
PaperT() : token(e_INVALID), name("invalid"), height(0), width(0)
{}
std::string ToString()
{
std::ostringstream stm;
stm << name << ", height: " << height << ", width: " << width;
return stm.str();
}
};
// Useful mappings. Paper information can now be
// efficiently looked up by token or by name.
extern std::map<PaperSizeE, PaperT> mapByToken;
extern std::map<std::string, PaperT> mapByName;
The next file contains the definitions for the maps declared above. Again the elements of the initiailzer list are constructed from the (multiple times) included macro header, with the respective suitable macro definitions. Here, too, trailing commas are ignored.
//////////////////////////////////////////////////////////////////
// paperdefs.cpp
//////////////////////////////////////////////////////////////////
#include "papers.h"
using namespace std;
std::map<PaperSizeE, PaperT> mapByToken
{
# define PAPERSIZE_MACRO(token, name, height, width) \
{e_##name, PaperT(e_##name, #name, height, width) },
// this expands to a series of
// { e_xx, PaperT(e_xx, "Ax", hhh, www) },
// which is used to initialize the entries a map enum -> paper.
# include "papermacros.h"
# undef PAPERSIZE_MACRO
};
std::map<string, PaperT> mapByName =
{
# define PAPERSIZE_MACRO(token, name, height, width) \
{#name, PaperT(e_##name, #name, height, width) },
// this expands to a series of
// { "Ax", PaperT(e_xx, "Ax", hhh, www) },
// which is used to initialize the entries a map name -> paper.
# include "papermacros.h"
# undef PAPERSIZE_MACRO
};
Last not least a main function in order to demonstrate usage.
//////////////////////////////////////////////////////////////////
// main.cpp
// Demonstrate how to use the paper related data structures.
// Must be linked with paperdefs.o
//////////////////////////////////////////////////////////////////
#include "papers.h"
#include <iostream>
using namespace std;
int main()
{
{
PaperSizeE a4Token = e_A4;
cout << "The name of the paper with token " << a4Token
<< " is " << mapByToken[a4Token].name << endl;
}
{
string name = "A3";
cout << "The token val of the paper named " << name
<< " is " << mapByName[name].token << endl;
}
// iterate over all papers
for(auto &p: mapByName)
{
cout << "Paper by name " << p.first << ": "
<< p.second.ToString() << endl;
}
}
The result:
$ g++ -std=c++11 -o papers main.cpp paperdefs.cpp && ./papers
The name of the paper with token 4 is A4
The token val of the paper named A3 is 3
Paper by name A3: A3, height: 400, width: 300
Paper by name A4: A4, height: 300, width: 200
Paper by name A5: A5, height: 200, width: 150
std::vector. Done.