Simple things are simply the most useful things. But one has to be careful not to make them simpler than that.
A co-worker / C++ student, came to me the other day, puzzled. Her code with cruft removed:
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 |
template< typename T > struct data final { using type = data; using value_type = T; static value_type store(const T& new_val) noexcept { type::last_ = new_val; return type::last_; }; // just read the stored value static value_type read(void) noexcept { return type::last_; } private: inline static value_type last_{}; }; // eof data int main(void) { using dbj::data ; using yes_data = data<bool> ; using no_data = data<bool> ; yes_data::store( true ); no_data::store( false ); // this assert has failed! assert( yes_data::read() != no_data::read() ) ; return 42; } |
How did she get into this mess?
Simple. That is a type that has methods and data all static. That means all is shared on the level of the type. Not instances. There is even no point in making an instance of that class
1 2 3 4 5 6 |
// data<bool> flag_ ; // does not compile // store is not available to instances // it is type method flag_.store( true ); |
There are valid situations approving of that design. Without questioning it we went down the path to the solution at the other end of the garden.
Not patient? Not a problem. Github repo is here. For the code regarding this post please look into dbj_nifty_store.h
Aiming for the happy end
In this part of the scenario, the key actor is very simple and very resilient C++. We will develop:
A single template whose definition creates all static types, holding a single piece of data, uniquely identifiable and thread resilient.
This is the core of the modern set of wrappers around your legacy data. And the usage is deceptively simple.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// using the new template to define two types // holding the distinctive bool values using store_a = dbj::data<bool, guid_a, dbj::padlock >; using store_b = dbj::data<bool, guid_b, dbj::padlock >; // silly example int fty2 = 42; int th_teen = 13; // store is used as a type not as a instance store_a::store(fty2); store_b::store(th_teen); // returns 42 std::cout << "\nA Stored: " << store_a::read(); // returns 13, from another store of the same type std::cout << "\nB Stored: " << store_b::read(); |
Simple, small resilient, and fast. That is the ideal type to extend it and add for example “atomicity” or knowledge of persistent storage without changing the original type.
Same type many stores
What are those guid_a
and guid_b
template arguments? They are identity providers. Simple functions returning dbj::GUID
.
Remember one data<>
type stores only one single piece of data. And that template definition is a class with only statics inside.
To keep the store simple and to differentiate between stores we add the second template argument as the unique identifier of the type to be defined when the template gets arguments. Consider this.
1 2 3 4 5 |
// not good template <typename T> struct data final { inline static T data ; } ; |
Now using the above I want to have two storages of bool
data. How do I do that?
1 2 3 4 |
// A: instances are redundant // B: both 'one' and 'two' hold the same piece of data data<bool> one ; data<bool> two ; |
The only way out is to add some kind of identification without abandoning the static data concept.
1 2 3 4 5 6 |
// identification parameter added template <typename T, identity_type uid > struct data final { inline static T data ; } ; |
The unique identity of the type defined is the role of the second argument in the data<>
template. And since up to C++20 concrete type as a template argument can not be a class we are using function pointers.
Synopsis. Hint: see the code online.
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 |
struct padlock final { using type = padlock; inline static std::mutex protector_{}; // protects last_ using guard = std::lock_guard<std::mutex>; padlock() { guard lock(type::protector_); } }; struct nolock final { using type = nolock; nolock() = default; }; using guid_source = dbj::GUID(*)(); // this class has only one static data member and no methods // the type contains all the functionality // not instances template< typename T, // at compile time function pointer itself is // giving unique ID not its call result // at runtime store_id_ provides the actuall GUID guid_source store_id_, // by default we de not lock typename LOCK = nolock > struct data final { using type = data; using value_type = T; using lock_type = LOCK; // not before this point we use the result of the // guid_source function static dbj::GUID store_guid() noexcept { LOCK guard; return store_id_(); } // warning C4101 // store new value static value_type store(const T& new_val) noexcept { lock_type guard; type::last_ = new_val; return type::last_; }; // disallow temporaries static value_type store(T&&) = delete; // just read the stored value static value_type read(void) noexcept { lock_type guard; return type::last_; } // this is no instances type // thus we will stop that nonsense ;) data() = delete; ~data() = delete; data(data const&) = delete; data& operator = (data const&) = delete; data(data&&) = delete; data& operator = (data&&) = delete; private: inline static value_type last_{}; }; // data |
store_a
and store_b
are two distinct types.
1 2 3 4 |
// define two types // holding two distinctive values using store_a = dbj::data<bool, guid_a, dbj::padlock >; using store_b = dbj::data<bool, guid_b, dbj::padlock >; |
The dbj::data<>
template when defined is a type that has only one static data member and no methods, ctors or dtors.
Instance less programing
Recap. The template definition is a type. And there is no point in making instances of the dbj::data<T>
type. Everything is static inside.
1 2 3 |
// single data instance also thread resilient // stored in a type, not in a instance using store_b = data<my_very_complex_type , guid_b, dbj::padlock >; |
Thus there is no possibility (and no need) for passing instances as e.g. function arguments. It is enough to pass the type only
1 2 3 4 5 6 7 8 9 10 11 |
// example template <typename STORE> inline void store_user ( typename STORE::value_type const & new_val ) { // STORE type STORE::store( new_val ); } // usage store_user<store_b>( 13 ); // returns 13 assert( 13 == store_b::read() ); |
Above is all zero overhead.
In the code ( file dbj_nifty_store.h
) there is one simple usage example.
The resilient identification
dbj::GUID
the suite will be detailed in the next post. The testing function is on the bottom of the same header. I will just mention the unique identification available.
1 2 3 4 5 |
inline void test_dbj_data_store() { auto guid1 = store_a::store_guid(); auto guid2 = store_b::store_guid(); } |
That is not a mockup. That is real UIID
aka GUID
. The value of that solution will be much more obvious with complex types handled in your complex scenarios. Hint: for example transaction processing.
You will easily add more functionality by adding functions around it, without touching the core template itself.
Github repo is here. It contains also the code from the next post on this GUID used here.