Ich möchte das Thema enum nochmal aufgreifen.
Ich habe einen Anwendungsfall - die eModbus Library -, bei dem es sogenannte "function codes" gibt. Theoretisch von 1..127, und jeden darf es nur einmal geben. Es gibt 38 vom Modbus-Standard vorgegebene Codes, die nicht lückenlos angeordenet sind, sondern ziemlich verstreut über die 127 möglichen Werte.
Die naheliegende Implementierung ist dann ein enum wie:
enum FunctionCode : uint8_t {
ANY_FUNCTION_CODE = 0x00, // Only valid for server to register function codes
READ_COIL = 0x01,
READ_DISCR_INPUT = 0x02,
READ_HOLD_REGISTER = 0x03,
READ_INPUT_REGISTER = 0x04,
WRITE_COIL = 0x05,
WRITE_HOLD_REGISTER = 0x06,
READ_EXCEPTION_SERIAL = 0x07,
DIAGNOSTICS_SERIAL = 0x08,
READ_COMM_CNT_SERIAL = 0x0B,
READ_COMM_LOG_SERIAL = 0x0C,
WRITE_MULT_COILS = 0x0F,
...
Klappt soweit natürlich auch.
Jetzt gibt es Modbus-Geräte, die sich nicht an den Standard halten, sondern eigene Function Codes definieren, die nicht in den vom Standard vorgegebenen Bereichen liegen, sondern "illegale" Werte nutzen.
Die kann die Library nicht per se unterstützen, weil niemand weiß, was da alles kommen kann. Um die aber trotzdem einzubringen, müssen die Anwender bis dato das enum in der Library manipulieren und ihre eigenen Codes hinzufügen, damit der Rest (der aus gutem Grund den Datentyp FunctionCode benutzt) der Library damit hantieren kann. Doof, eigentlich.
Deswegen habe ich mir ein anderes Konzept überlegt und möchte es hier mal vorstellen. Grundidee ist es, für jeden Funktionscode eine Singletoninstanz einer FunctionCode-Klasse zu erzeugen, so dass die Library ihre Standardcodes vorgeben kann, Anwender aber typ-konform eigene dazu definieren. Und jeder Versuch, einen bereits existierenden Code erneut zu definieren, läuft auf eine reine Umbenennung desselben Codes im Userspace heraus, ohne dass die Originaldefinition damit zerstört würde.
Etwas kompliziert und braucht je Code auch mindestens ein uint8_t Speicher, kommt aber dem "erweiterbaren enum" recht nahe.
Hier der Code dazu:
#include <cstdio>
#include <cstdint>
#include <cstdlib>
#include <unordered_map>
#include <iostream>
#include <iomanip>
using std::unordered_map;
using std::cout;
using std::endl;
// FunctionCode: The interface to access the function code methods.
// block() and private constructor will prevent direct instances
// New function codes will only be created with the NewFC template class (see below)
class FunctionCode {
protected:
const uint8_t fc; // The function code
static unordered_map<uint8_t, FunctionCode*> codes; // Map of all codes so far
virtual void block() = 0; // virtual function to prevent direct instances
FunctionCode(uint8_t x) : fc(x) { // Constructor accepting a uint8_t code
// Collect code in map
codes[x] = this;
}
virtual ~FunctionCode() { // virtual destructor not to be called directly
// Drop function code from map
codes.erase(fc);
}
public:
FunctionCode() = delete; // No empty constructor
FunctionCode& operator=(const FunctionCode& other) = delete; // No copy constructor
operator auto() const { return fc; } // Get function code as uint8_t
// code(uint8_t f) shall give access to a known code or code 0 instead
static const FunctionCode& code(uint8_t f) {
unordered_map<uint8_t, FunctionCode*>::const_iterator got = codes.find(f);
if (got == codes.end())
return *codes[0];
else
return *got->second;
}
};
// NewFC<n> will create a singleton instance for code <n> derived from FunctionCode
template <uint8_t f>
class NewFC : public FunctionCode {
private:
// Private constructor will only be called internally
NewFC() : FunctionCode(f) {
static_assert(f < 128, "FunctionCode must be in [1..127].");
}
// Define virtual FunctionCode::block() to make class instantiable
void block() { }
~NewFC() { }
public:
// Only method seen from outside is instance()
// Will return (and in case create) a single instance of the given code
static NewFC& instance() {
static NewFC Instance;
return Instance;
}
};
// Initialize static map
unordered_map<uint8_t, FunctionCode*> FunctionCode::codes;
// Create default function code 0
FunctionCode& NoCode = NewFC<0>::instance();
// tcode() is to test FunctionCode creation in non-global domain
void tfunc() {
FunctionCode& D = NewFC<3>::instance();
}
int main(int argc, char **argv) {
// Create some instances
FunctionCode& A = NewFC<1>::instance();
FunctionCode& B = NewFC<2>::instance();
FunctionCode& C = NewFC<2>::instance(); // Second use - shall return the same instance as B!
cout << "B is " << (B != C ? "not " : "") << "identical to C" << endl;
// Print out all instances known yet
cout << "pre tfunc()" << endl;
for (uint8_t i=0; i<5; ++i) {
cout << int(i) << ": '" << int(FunctionCode::code(i)) << "'" << endl;
}
// Call tfunc() to create another FunctionCode
tfunc();
// Print out all instances now existing
cout << "post tfunc()" << endl;
for (uint8_t i=0; i<5; ++i) {
cout << int(i) << ": '" << int(FunctionCode::code(i)) << "'" << endl;
}
return 0;
}
Die Ausgabe des Programms ist dann
B is identical to C
pre tfunc()
0: '0'
1: '1'
2: '2'
3: '0'
4: '0'
post tfunc()
0: '0'
1: '1'
2: '2'
3: '3'
4: '0'
Man kann also irgendwo neue Codes definieren, die dann global zur Verfügung stehen, und bestehende nicht überschreiben.