Chi-Tech
devman_01_06_StaticRegistration.h
Go to the documentation of this file.
1/**\page DevManStaticRegistration Static Registration
2
3\tableofcontents
4
5This page explains the details of the two utility classes `ParameterBlock` and
6`InputParameters`.
7
8\section DevManParametersSec1 1 The general philosophy of Static Registration
9
10First, what is <B>Static Registration</B>? When a c++ program is executed there
11are a number of phases that occur in order. Once such phase, before entering the
12`main` function, is the phase in which all static variables are initialized. We
13shall call this phase the static initialization phase.
14In the most basic sense static registration is the process whereby we create a
15registry of object-constructors during this phase. This registry can then used,
16during the dynamic phases of the program, to construct objects.
17A simple example is the code below:
18\code
19#include <iostream>
20#include "DoSomethingFunctions"
21
22static int a = DoSomething();
23
24int main()
25{
26 std::cout << "The value of a is: " << a << std::endl;
27}
28\endcode
29where the variable `int a` is initialized, before the `main` function starts,
30using a function call, which happens during the static initialization phase of
31the program.
32
33\subsection DevManParametersSec1_1 Rigging static initialization for object factories or object makers
34
35We can use the rather odd dynamics of singleton objects to create a static
36registry of function creation calls during static initialization. In the code
37below we define an `ObjectMaker` class that is a singleton. A singleton is an
38object of which only a single instance can be created, which we do by declaring
39a static instance, a bunch of static members methods, a private constructor and
40deleted copy- and move constructors.
41
42The `ObjectMaker` maintains a map of string keys to object construction calls.
43The construction calls always return a pointer to a base class `Object` but can
44construct any child object by using the shown template arguments. In order for
45us to achieve basic static registration we need two templated-methods, these
46are `CallObjectConstructor` and `RegisterObject` in the code below.
47The first method defines a function-address that we can put into our registry.
48The second method is the actual method we call to make a registry entry. When we
49then want to make a registered object we simply call the `MakeRegisteredObject`
50method.
51
52\code
53#include <iostream>
54#include <map>
55#include <functional>
56
57class Object
58{
59public:
60 int member_;
61 Object() : member_(99) {}
62 Object(int member) : member_(member) {}
63};
64
65class ChildObject : public Object
66{
67public:
68 ChildObject() : Object(999) {}
69};
70
71//===============================================
72typedef std::function<Object*()> CreationFunction;
73class ObjectMaker // Singleton
74{
75public:
76 std::map<std::string, CreationFunction> registry_;
77
78public:
79 static ObjectMaker& GetInstance() noexcept
80 {
81 static ObjectMaker singleton;
82 return singleton;
83 }
84 template <typename T>
85 static Object* CallObjectConstructor() { return new T(); }
86
87 template <typename T>
88 static int RegisterObject(const std::string& register_name)
89 {
90 auto& instance = GetInstance();
91 instance.registry_.insert(
92 std::make_pair(register_name, &CallObjectConstructor<T>));
93 return 0;
94 }
95
96 static Object* MakeRegisteredObject(const std::string& obj_name)
97 {
98 auto& instance = GetInstance();
99
100 return instance.registry_.at(obj_name)();
101 }
102private:
103 ObjectMaker() = default;
104 ObjectMaker(const ObjectMaker&) = delete;
105 ObjectMaker(const ObjectMaker&&) = delete;
106 ObjectMaker& operator=(const ObjectMaker&) = delete;
107};
108
109//===============================================
110// This is where we statically register an object
111static int abc12345 = ObjectMaker::RegisterObject<Object>("Parent");
112static int abc12346 = ObjectMaker::RegisterObject<ChildObject>("Child");
113
114
115int main()
116{
117 Object objectA(98); // Conventional object creation
118 auto objectB = ObjectMaker::MakeRegisteredObject("Parent");
119 auto objectC = ObjectMaker::MakeRegisteredObject("Child");
120
121 std::cout << "Object A.member_ = " << objectA.member_ << "\n";
122 std::cout << "Object B.member_ = " << objectB->member_ << "\n";
123 std::cout << "Object C.member_ = " << objectC->member_ << "\n";
124
125 return 0;
126}
127\endcode
128Output:
129\verbatim
130Object A.member_ = 98
131Object B.member_ = 99
132Object C.member_ = 999
133\endverbatim
134
135Notice that we abuse the static initialization phase right above the `main`
136function. We call the static singleton `ObjectMaker` and `RegisterObject` to
137create registry entries for a `Parent` and a `Child`. In the main function we
138then construct some of these statically registered objects.
139
140\section DevManParametersSec2 2 Implementation in ChiTech: ChiObject and ChiObjectFactory
141In ChiTech we have to, of course, enhance the functionality by a lot, to this
142end we have the base object, `ChiObject`, and the factory, `ChiObjectFactory`.
143Also, instead of using raw pointers, we use shared pointers for all
144objects that can be statically registered.
145
146\image html "DeveloperManual/StaticRegistration/StaticRegistration.png" "The factory pattern" width=900px
147
148We developed a separate system to
149control object construction parameters (more on this later). Additionally,
150we do not want developers to come up with unique dummy variable names for the
151static registration, i.e.,
152`static int abc12345` or `static int abc12346`, just to make use of the
153`ChiObjectFactory`, therefore, we use a macro. That works as follows.
154
155In order to register an object within the `ChiObjectFactory` we created a macro
156called `RegisterChiObject(namespace_name, obj_name)`. The arguments to this
157macro, `namespace_name` and `obj_name`, then get used to construct the following
158\code
159RegisterChiObject(namespace_name, ObjectName);
160//
161// becomes:
162//
163static char unique_var_name_object_ObjectName_0 =
164 ChiObjectFactory::AddObjectToRegistry<ObjectName, ChiObject>("namespace_name",
165 "ObjectName")
166\endcode
167which allows us to abuse static initialization to register the object with
168text-name `"namespace_name::ObjectName"`. Note that nested namespaces are
169allowed, e.g., `"space1::space1b::space1bc"`, is allowed.
170
171We also implemented a number of "tricks". For example we created a
172behind-the-scenes connection to the lua console so that registered objects can
173be created from the lua input system. We also created a "RegistryDump" where
174each registered object can dump its input arguments specification so we can use
175it to build documentation (lessening the burden on the developer).
176
177\section DevManParametersSec3 3 The InputParameters system
178All statically registered objects have a common input parameters interface
179implemented in the general class `chi::InputParameters`. This allows
180us to fully support static registration if an object has the following basic
181structure
182\code
183#include "ChiObject/chi_object.h"
184
185namespace zorba
186{
187
188class CoolObject : public ChiObject
189{
190 const int a_;
191 const double b_;
192 //Misc. members
193public:
194 static chi::InputParameters GetInputParameters();
195 CoolObject(const chi::InputParameters& params);
196
197 //Misc. methods
198};
199
200}
201\endcode
202Here we have the static member method `GetInputParameters` which returns a
203`chi::InputParameters` object used to assign parameters from user input,
204and the constructor `CoolObject(const chi::InputParameters& params);`
205which uses such an object.
206
207In the definition (`.cc`) file we then have
208\code
209#include "CoolObject.h"
210
211#include "ChiObject/object_maker.h"
212
213namespace zorba
214{
215
216RegisterChiObject(chi_unit_tests, CoolObject);
217
218chi::InputParameters CoolObject::GetInputParameters()
219{
220 chi::InputParameters params = ChiObject::GetInputParameters();
221
222 params.SetGeneralDescription("A Cool object to play with");
223 params.SetDocGroup("doc_Coolstuff"); // Documentation link
224
225 params.AddRequiredParameter<int>("a", "Coolness factor a");
226 params.AddOptionalParameter("b", 1.0, "Coolness factor b");
227
228 return params;
229}
230
231CoolObject::CoolObject(const chi::InputParameters& params) :
232 ChiObject(params),
233 a_(params.GetParamValue<int>("a")),
234 b_(params.GetParamValue<double>("b"))
235{
236}
237
238}//namespace zorba
239\endcode
240
241Creating this object from lua is then as easy as
242\code
243params = {a = 98, b = 99.0}
244cool_obj = zorba.CoolObject.Create(params)
245\endcode
246
247\subsection DevManParametersSecFAQ1 FAQ1 Documentation-strings (Doc-strings)
248All parameters must have a documentation string (short name "doc string").
249For example:
250\code
251params.AddRequiredParameter<int>("a", "Coolness factor a");
252params.AddOptionalParameter("b", 1.0, "Coolness factor b");
253\endcode
254here "Coolness factor a" and "Coolness factor b" are the doc strings for
255parameters "a" and "b" respectively.
256
257You can also add documentation for the entire object. When the documentation
258is auto-generated this description will appear at the top.
259\code
260params.SetGeneralDescription("General test object");
261\endcode
262
263By default the object's documentation will be posted to a group that is
264reference-able via doxygen as `<namespace_name>__<ObjectName>` (note the double
265underscore is a replacement for the namespace scope clarifiers `::`). You can
266link the documentation to other groups by using the `SetDocGroup()`
267method. This functionality allows you to customize your documentation according to your
268needs.
269
270<B>Note:</B> The block of text supplied to `SetGeneralDescription` is pasted
271into a documentation page that will be processed by doxygen so all doxygen
272commands will work here (including formulas, code blocks, and links).
273
274\subsection DevManParametersSecFAQ2 FAQ2 Optional Parameters
275Optional parameters must always have a default value. However, you do not
276necessarily have to use it. In cases where you use a parameter only when
277supplied,
278the `InputParameters` object has the interface method `ParametersAtAssignment`
279which returns the parameters used at assignment. It is used as follows:
280\code
281CoolObject::CoolObject(const chi::InputParameters& params) :
282 ChiObject(params),
283 a_(params.GetParamValue<int>("a")),
284 b_(0.0)
285{
286 const auto& user_params = params.ParametersAtAssignment();
287 if (user_params.Has("b"))
288 b_ = user_params.GetParamValue<double>("b");
289}
290\endcode
291
292\subsection DevManParametersSecFAQ3 FAQ3 Inheriting parameters from parent objects
293Input parameters can be inherited from parents in the `GetInputParameters`
294method call. For example
295\code
296chi::InputParameters LBSSolver::GetInputParameters()
297{
298 chi::InputParameters params =
299 chi_physics::Solver::GetInputParameters();
300 params += chi::SecondParent::GetInputParameters();
301 //
302 //etc.
303\endcode
304Gets all the parameters from the parent object `chi_physics::Solver` and then
305`chi::SecondParent`, allowing us to change or add to these parameters.
306
307In fact, a pretty slick move here is that we are not bound by classical
308inheritance. The input parameters of any statically registered object can be
309snatched and used by simply calling its static `GetInputParameters` method.
310
311\subsection DevManParametersSecFAQ4 FAQ4 Changing inherited options to optional/required
312If a required parameter is to be changed to optional we can do this using the
313interface `ChangeExistingParamToOptional`, e.g.,
314\code
315chi::InputParameters LBSSolver::GetInputParameters()
316{
317 chi::InputParameters params =
318 chi_physics::Solver::GetInputParameters();
319
320 params.ChangeExistingParamToOptional("name", "LBSDatablock");
321\endcode
322
323Vice versa there is also `ChangeExistingParamToRequired<type>("name")`.
324
325\subsection DevManParametersSecFAQ5 FAQ5 Deprecated parameters
326Parameters in an `InputParameters` block can be marked as deprecated via 3
327different mechanisms:
328- Marking them with a deprecation warning `MarkParamaterDeprecatedWarning`. This
329 will generate just a warning if the parameter is supplied..
330- Marking them with a deprecation error `MarkParamaterDeprecatedError`. This
331 will generate an error if the parameter is supplied..
332- Marking them with as renamed `MarkParamaterRenamed`. This will generate an
333 error if the parameter is supplied if the parameter is supplied.
334
335\subsection DevManParametersSecFAQ6 FAQ6 Constrained ranges
336Parameters in an input parameters block can be constrained to the following
337- `chi_data_types::AllowableRangeList`, created with a `std::vector` of a
338 specific type. The parameter can then only be one of those types.
339- `chi_data_types::AllowableRangeLowLimit`. Parameter must be >= than the
340 value specified when the range is specified as closed or just > that the value
341 specified when the range is specified as open.
342- `chi_data_types::AllowableRangeHighLimit` same as the low limit but just with
343 a smaller than comparison.
344- `chi_data_types::AllowableRangeLowHighLimit` combination of both a high and
345 a low limit range.
346
347Example:
348\code
349chi::InputParameters StrangeSolver::GetInputParameters()
350{
351 chi::InputParameters params =
352 chi_physics::Solver::GetInputParameters();
353 params.AddRequiredParameter<std::string>(
354 "sdm_type", "The spatial discretization type to be used");
355
356 using namespace chi_data_types;
357 params.ConstrainParameterRange(
358 "sdm_type", AllowableRangeList::New({"FV", "PWLC", "PWLD"}));
359 // etc.
360\endcode
361*/