
Some real life context to this post might be here. And what is going on in here? Conceptually.Structs with only static methods, when instantiated, are resulting in all instances sharing the same methods inside.
1 2 3 4 5 6 |
// no real identity struct all_shared final { // shared among all instances static bool m1 () { return true; } }; |
Same as static members, static methods are shared between all instances. Meaning: no actual different objects can be created out of the above.
1 2 3 4 5 6 |
all_shared o1, o2, o3 ; // all calling the same method // all_shared::m1() o1.m1() ; o2.m1() ; o3.m1() ; |
Those objects technically are different. But methods are not. Them objects all share the same method. Very much like having the namespace with a function inside.
Now if we make this into the template we suddenly raise the complexity.
1 2 3 4 5 6 |
template< bool argument > struct half_shared final { // shared among all template instances static bool m1 () { return argument; } }; |
A template is not a type. From the above template, we can make only two types aka template definitions.
1 2 |
using true_half = half_shared<true> ; using false_half = half_shared<false> ; |
Now we have two types. Each type having only one shared method inside. Both being a method m1()
declared in the template. Thus:
1 2 3 4 5 6 7 8 |
true_half t1, t2 ; false_half f1, f2 ; // sharing true_half::m1() t1.m1(); t2.m1(); // sharing false_half::m1() f1.m1(); f2.m1(); |
The outcome of using a template is we got different types. We share m1()
on the type level. Because it is static. Thus we are narrowing the scope of sharing. By using particular template arguments.
C++ complexity is a direct consequence of C++ flexibility. And sometimes we need it.
A real-life use case.
I had the same problem in my dbj SQL suite. An SQLite standard C++ API. A simple template with “nothing” inside that has to be made into separate unique types when defined. I had to narrow the scope of sharing of a static method inside a template.
UDF is user-defined function. Crucial for the whole dbj SQL framework is to offer “simple udf’s” functionality for the customers. And I wanted to encapsulate this, in order to decouple the users from a complex sqlite3 C API.
SQLite API provides one function to register UDF’s. And it is to be used like so:
1 2 3 4 5 6 7 8 9 10 |
// SQLite C API sqlite3_create_function( handle.get(), /* database handle */ udf_name.data(), /* udf name for SQL statements */ 1, SQLITE_UTF8, NULL, udf_, /* the user provided udf */ NULL, NULL); |
Where udf is a C/C++ function pointer, that must conform to exact signature required.
To use this in it’s “naked” form, for 99% of users is “not fun”. And hardly leads to reusable code. Thus they resort to C++ wrapper like mine is.
So, what is the point?
What I am sharing here is how I have designed and developed this “easy UDF’s”. Let’s start with the key C++ abstraction.
1 2 3 |
// dbj "easy udf" signature using dbj_sql_udf_type = void(*) (const udf_argument &, const udf_retval &); |
Users see and use the above. Using that, inside my implementation, I have to call the bellow, eventually.
1 2 3 |
// sqlite required udf signature using sqlite3_udf_type = void(__cdecl *) (sqlite3_context *context, int argc, sqlite3_value **argv); |
I wrap the instance of the “easy udf” above, to be used by the required template definition.
Key point: we give exact function pointer of “easy udf” as template argument, thus different types are created when this template is turned into a type.
The template.
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 |
// argument is not a type but a function pointer // the "easy udf" as above template<dbj_sql_udf_type udf_> struct udf_holder final { // this is called by SQLite // I pass this method as the required callback // to the SQLite C function, thus // this method must be static so we can use it in // C API, as function pointer required static void function (sqlite3_context *context, int argc, sqlite3_value **argv) { // this is my code and my types // not SQLite udf_argument values_{ argv, size_t(argc) }; udf_retval result_{ context }; // here I am // calling user defined function // aka udf udf_(values_, result_); } }; |
A template with just one static function inside. Let me repeat: that one template argument is not a type, it is a value argument. A function pointer type.
Why is it done this way?
Repetition: A template in itself is not a type. Types are made from templates that are given concrete template parameters and turned into template definitions. Template definition is a type. Function pointer as a template argument is providing identity to the definitions made from this template. Example.
1 2 3 4 5 6 7 |
// declarations of two udf's void udf_one (const udf_argument &, const udf_retval &); void udf_two (const udf_argument &, const udf_retval &); // two distinctive definitions as two types // made from the same template using udf_h_1 = udf_holder<udf_one> ; using udf_h_2 = udf_holder<udf_two> ; |
udf_holder
template, has no data and no functions, only one static method. To be shared among all types made from it.
The template argument is a function pointer. Each udf is a unique function pointer, thus providing a unique identity to the types udf_h_1
and udf_h_2
above.
Crucial point:
udf_h_1
andudf_h_2
are two completely different types.
Ok, but why is this function static?
Because we need to cast it to non-member aka “free-standing” function pointer type. And that function is required by SQLite3 C API. How do we transform it from C++ static template function to free-standing function:
1 2 3 |
// assign address of C++ class static function, to // the required C API function pointer type sqlite3_udf_type udf_ = & udf_h_1::function; |
Address of C++ class static function can be assigned to C function pointer. You can not do that with “normal” non-static C++ class function.
The API
Basically, I have a standard callback function, that is always the same. From which, in turn, I call different SQLite user-defined “easy udf” functions. Here is the one method from dbj++sql.h in my SQLite C++ API that does the transformation into C function and registration with the SQLite:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
template< // pass the exact and unique udf dbj_sql_udf_type dbj_udf_ > inline void register_dbj_udf( // the sqlite database handle const dbj::db::database & db, const char * dbj_udf_name_ ){ // create unique udf_holder type using udf_container_type = udf_holder<dbj_udf_>; // assign it's single static function // to C API function type required sqlite3_udf_type udf_ = &udf_container_type::function; // send it for registration // with required name db.register_user_defined_function( dbj_udf_name_, udf_); }; |
All of the above C++ sorcery is hidden away and happy users make happy standard C++ functions to be used as SQLite “easy udf’s”, each in one simple call:
1 2 3 4 5 6 |
// using namespace dbj::db ; // add two easy udf's to this db register_dbj_udf<udf_one>( db, "function_1"); register_dbj_udf<udf_two>( db, "function_2"); // |
Same as ever, if anybody requires more clarifications, do not be afraid to comment. Enjoy the standard C++.