All entries for Wednesday 30 October 2024
October 30, 2024
Another trick for nice wrappers
Follow-up to A tricksy bit of C++ templating from Research Software Engineering at Warwick
In the previous post we wrote a lot about using templating in order to write very powerful wrapper classes that could relatively easily expose function from what they are wrapping. This time, we're showing one final piece of that puzzle, which is how to wriggle around the wrapper, without just abandoning type safety.
Suppose we have a class in C++. We are allowed this mad looking construct:
struct myClass{
int member; // Data member
void otherMember(int a){std::cout<<"Called the fn on "<<member<<" with "<<a<<'\n';}
};
int main(){
myClass tmp;
// Pointer to class member
int myClass::*tmpMem = &myClass::member; //<<<<<-----
// Access member of specific instance
tmp.*tmpMem = 10;
// Function pointer to class member function
// Best to use auto, but doing it long-form to demonstrate
void (myClass::*tmpFn)(int) = &myClass::otherMember; //<<<<<-----
// Call the function. Note the brackets
(tmp.*tmpFn)(2); //<<<<<-----
}
Here, we're getting a "pointer-to-member", either a data member, or a function, of a specific class. Notice the difference between the type of tmpMem, which is a pointer to the member element of some instance of myClass, but not any specific instance. To use the pointer, we have to apply to a specific instance. In a lot of senses, you can think of this as containing the instruction to get to a particular member given the start of the object in memory, and for data there's a fair chance it unpacks to something like this behind-the-scenes.
For a function, the same applies. tmpFn is a pointer to the otherMember function of some instance of myClass. We can call the function on a particular instance. Note the brackets - we need to bracket the whole "callable" chunk on the left, and also supply the relevant arguments. If you get errors about something being "not callable", you probably forgot or misplaced these.
Aside: If the class, or the function is templated, then type deduction will work as usual. If you need to specify a type explicitly, you do it about how you'd expect, like:
void (myClassT<double>::*tmpFnT2)(int) = &myClassT<double>::otherMember;
myClassT<double> tmpT;
(tmpT.*tmpFnT2)(2);
So, why would we ever want to do this? Well, two reasons really.
On the one hand, it's about making the type system work for us. A simple pointer to an int, or a function taking an int and returning void, are very general. A class scoped variant is more restrictive, and lets us enforce that only members of a particular class are valid.
On the other hand, this construct lets us do some things that we could otherwise only do by reflection, usually involving wrappers. We showed some pretty horrible templating in our previous post involving wrapping specific named functions. This idea lets us write something to invoke an arbitrary function on a class we're wrapping, while retaining some type safety and access control (i.e not just making it public). Read on to see how.
Implementing a pass-though call
For simplicitly, we're going to show the example for a target function which takes one argument and may or may not return anything. A variadic template, in place of the typename T in the definition of invoke, lets us take any number, of any kind, with some caveats about references. (If we get the chance, we might write about argument forwarding in future).
So suppose the wrapper class we're writing wraps some inner type. Suppose we're free to substitute anything we want as the inner type, but not allowed to modify the wrapper class, or more usually, that we don't want the wrapper to become unmanageable. Further, suppose the wrapper has some reason to exist, like to log all calls, apply some kind of checks etc.
In this case, what we need, is the ability to pass the wrapper a function to call, and have it do it's "wrapper-y" work before passing this on. If this "wrapper-y" work needs any parameters, we can obviously provide them. What we need, is the pointer-to-member idiom, like this:
#include<iostream>
struct ST{
void theFn(int a){std::cout<<a<<std::endl;}
};
public:
template<
typename fn, typename T>
auto invoke(fn callable, T arg){
return (theVal.*callable)(arg);
}
};
int main(){
wrapper myW;
auto fn = &ST::theFn;
myW.invoke(fn, 10);
}
Now that works, but it's a little bit ugly in "user-space". The problem I was doing this to solve was to let me define a few, specific functions on the ST type, interject some checking via the wrapper, and not have to expand my wrapper to handle any possible function I might name. While it's not perfect, if I just define the function below, I am pretty close to being able to do this seamlessly, without a "user" having to know what's going on. Note I have named these to be explanatory, and wouldn't suggest being so heavy handed in reality:
auto call_theFn(wrapper & theW, int theVal){
theW.invoke(&ST::theFn, theVal);
}
call_theFn(myW, 7)
Compare this to the seamless
myW.theFn(7)
and I think it's alright.
Aside: the variadic version, which is way more useful, is here. Note that if the function takes a value by reference, we have to do something more careful in forwarding our types. That gist should work and preserve reference, const etc.