Local functions
Local functions are supported through non-portable custom extensions in GCC
and Clang but are not part of the C++ standard.
They have however always been supported through static member functions of local
classes with visibility (closed) on all static
and thread_local
variables
declared in the enclosing scope.
Starting with C++11 local functions are also supported trough non-capturing
lambdas, which are also closed on thread_local
and static
local variables.
Local functions can be used to e.g. implement generators or return stateful
free functions interoperable with C
libraries.
Implementation
A counter generator looks like:
using Counter = int (*)();
Counter GenCounter(int start) {
thread_local int val = start;
struct __ {
static int _() { return val++; }
};
// C++11: return [](){ return val++;}
return __::_;
}
…used like this:
// generate counter starting at 10
auto count = GenCounter(10);
for(int i = 0; i != 10; ++i) cout << count() << " ";
which results in the following output:
10 11 12 13 14 15 16 17 18 19
Now, this works but what you cannot do is using the same generator function
to create multiple independent counters.
The following code will have both counter1
and counter2
increment the same
variable.
auto counter1 = GenCounter(2); // initialize counter variable to 2
auto counter2 = GenCounter(3); // initilize same counter variable to 3
cout << counter1() << " " << counter2() << endl;
output
3 4
instead of the desired 2 3
output.
One way to generare unique generators at compile time is to just add a template parameter to identify different instances.
template <int ID = 0>
Counter GenUniqueCounter(int start) {
thread_local int val = start;
struct __ {
static int _() { return val++; }
};
// C++11: return [](){ return val++;}
return __::_;
}
now the following code works as expected:
auto counter1 = GenUniqueCounter(2); // initialize counter variable to 2
auto counter2 = GenUniqueCounter(3); // initilize same counter variable to 3
cout << counter1() << " " << counter2() << endl;
output
2 3
Applications
This approach allows to e.g. implement Object Oriented concepts by storing
a local instance of the data inside an enclosing function and accessing the
instance through a standard C
struct
with function pointers or C++ classes
not containing any data members.
In the case of C
struct
s variables can be passed to any C
library and will
behave like stateful objects.
The following code implements a constructor returnin a C++ struct
containing
only function pointers and a couple of methods to perform type-safe access to
data.
// Create Object containing C function pointers to access the Sphere
// instance stored within the function
template <int ID>
Object MakeSphere(double radius, const Vec3D& position) {
thread_local Sphere s = {SPHERE, radius, position};
using ApplyFun = void (*)(Sphere*);
struct __ {
// return empty intersection data
static ISect Intersect_(const Ray& ray) { return ISect(); }
static void Apply_(void* f, const std::type_info& ti) {
if(!f || ti != typeid(Sphere)) throw std::bad_cast();
auto fun = reinterpret_cast<ApplyFun>(f);
fun(&s);
}
static void* Get_(const std::type_info& ti) {
if(ti != typeid(Sphere)) throw std::bad_cast();
return &s;
}
};
return {.Intersect = __::Intersect_, .CheckedApply = __::Apply_,
.CheckedGet = __::Get_};
};
Code implementing the discussed concepts and more is available here
Run-time instances
The above code can only generate instances at compile-time; run-time generation
requires storing instances in local data stores (e.g. unordered_map
)
and passing an instance id around.
One advantage of keeping all instances in one place outside the class used to access the data is the ability to use an external centralized (possibly remote) data storage, with the client code staying the same regardless of where the data is actually stored.
template <typename...ArgsT>
pair<InstanceID, Methods> GenInstance(TypeID type, ArgsT...initParameters) {
thread_local unordered_map<InstanceID, InstanceData> instanceStore;
const InstanceID id = NewID();
instanceStore[id] = CreateInstance(type, initParameters...);
struct __ {
static void Render(InstanceID id) { RenderFun(instanceStore[id]);}
static void Delete(InstanceID id) { if(Valid(id)) instanceStore.erase(id);}
...
};
return {id, {.Render = __::Render, .Delete = __::Delete,...}};
}
class Object {
template <typename...ArgsT>
Object(const ArgsT&...args) {
auto im = GenInstance(args...);
instance_ = im.first;
methods_ = im.second;
}
Object(Object&& other) : instance_(other.instance_) {
other.instance_ = InvalidInstanceID();
}
void Render() { methods_.Render(instance_); }
~Object() { methods_.Delete(instance_); }
private:
Methods methods_;
private:
InstanceID instance_;
}
The described access patterns is similar to the PImpl idiom but hiding even the instance address.
Pre-allocating memory and populating the local store with instances, and returning such instances at creation time, allows to avoid allocations at run-time.
Smart pointers and lock guards can be employed to allow the use of static
instead of thread_local
variables and sharing.