Contents:
using namespace xParam;
appears in the beginning of your file. This will make the syntax
simpler and the examples easier to follow.
Initializing the Package
Normally, you do not need to do anything special in order to start
working with XParam in your program. However, during installation,
the "--enable-explicit-init" option may have been activated. (see
Installing XParam). If this is the
case, you will need to run the following function:
xparam_init();
prior to any use of XParam in your program.
The purpose of "xparam_init()" is to make sure that all registrations have been completed before XParam is used. Typically, you'll want "xparam_init();" to be the first line of your "main()", causing XParam to be useable anywhere after the beginning of "main()". Before reaching "main()" it is impossible to ascertain that all the registration code has been reached, so using XParam may lead to erroneous results.
Forcing explicit manual initialization will cause usage
of XParam before calling
"xparam_init()" to be considered an error. If the explicit manual
initialization
option was not used during installation, XParam will not require the
use of "xparam_init()", and using it will have no effect.
Val and Var
The most basic concept in the XParam library is that objects of any
type can be serialized and deserialized.
Here's how this is done:
my_ostream << Val(my_variable);
my_istream >> Var(my_variable);
These two lines can be separated into two parts. In the first line, we first used the function Val on my_variable to get a "Value Source" variable. This is a type of variable that can output XParam-serialized data, using my_variable as its information source.
Alternatively, in the second line, we used the function Var on my_variable to get a "Value Sink" variable. This is a variable that can receive XParam-serialized data and initialize my_variable with it.
A "Value Source" can be serialized using "operator <<" in the usual way, and, as usual, the output operator can be chained. The same is true for using the input operator on "Value Sink".
The C++ type that corresponds to an XParam "Value Source" is a class called "Handle<ValueSource>", whereas the "Value Sink" C++ type is a class called "Handle<ValueSink>". So, in C++ terminology, the interfaces used so far for serialization and deserialization are:
template<class T>
Handle<ValueSource> Val(const T&);
std::ostream& operator<<(std::ostream&,const Handle<ValueSource>&);
template<class T>
Handle<ValueSink> Var(T&);
std::istream& operator>>(std::istream&,const Handle<ValueSource>&);
In this way, objects of all denominations can be handled in
XParam: classes, structs, built-in types and enums.
PtrVal and PtrVar
"PtrVal" and "PtrVar" are functions identical to Val and Var in their
usage. The only difference is that "PtrVal" and "PtrVar" should only be
used in conjunction with pointer types, whereas "Val"
and "Var" should never be used with pointer types. One way to view this,
is that PtrVal is used to reference exactly the same type as Val,
but does it through the use of a pointer. The same can also be said
about the relationship between Var and PtrVar.
Unlike Val, PtrVal is also capable of handling NULL.
The C++ programming language does not require us to use different names for "PtrVal" and "Val" (as well as for "PtrVar" and Var"). The language is strong enough to detect whether a pointer type is used or not, and to use the pointer functionality when it is required and the non-pointer functionality when it is required. We decided, however, to keep the pointers separated from the non-pointers for two reasons:
cout << Val(x);
cout << PtrVal(&x);
if "x" is a non-pointer variable.
PtrVal can be given a pointer to any type, be it a class, a struct,
a built-in type or an enumerator.
Saver and Loader
When we started testing XParam, we found that one use-case turned out
to be particularly common. People wanted to communicate between
programs using streams of objects: one program creates the objects
and outputs them onto a stream; the other reads the stream and uses
the objects. If this is what you need, here are two classes that
you might find useful. Saver and Loader are both very small and
very simple classes, but their functionality will make your
programming life simpler and your programs easier to read.
"Saver" is used for serialization and "Loader" for deserialization. The syntax of their use is this:
Saver(my_ostream) << Val(my_variable);
Loader(my_istream) >> Var(my_variable);
This is equivalent to:
my_ostream << Val(my_variable) << endl;
my_istream >> Var(my_variable);
As can be seen, "Saver" has the added functionality of adding "endl" after each variable, which is effective as an object separator.
The added functionality in "Loader" is the method "bool Loader::eof()" which eats white-space in the stream and returns whether EOF has been reached.
Here is an explicit listing of the C++ interface provided by the Saver and the Loader classes.
class Saver { public: explicit Saver(std::ostream& os); Saver operator<<(const Handle<ValueSource>&) const; }; class Loader { public: explicit Loader(std::istream& is); bool eof(); Loader operator>>(const Handle<ValueSink>&) const; };
As can be seen, Saver and Loader accept any "value source" and "value sink" variables, so PtrVal and PtrVar can also be used in this context.
Though it does not seem that Saver and Loader add much, they are very convenient to use when you need to handle streams of objects. Consider this program segment, for example
for(i=objlist.begin();i!=objlist.end();++i) { Saver(cout) << PtrVal(i); }
yields, on the standard output, a list of objects separated by linebreaks, which can be piped into a program with the following read loop:
Loader input(cin); while(!input.eof()) { input >> Var(i); ... // do whatever you want with "i" here. }
#include <string> #include <xparam.h> using namespace xParam; int main(int argc, char* argv[]) { std::string name; int age, experience; double height; bool result; ParamSet ps; ps << "This program determines whether the applicant will get the job." << iParamVar(name, "name ! Applicant's name") << iParamVar(age, "age ! Applicant's age") << iParamVar(height, "height ! Applicant's height in meters") << iParamVar(experience, "experience ! Years of experience the applicant has",Val(0)) << oParamVar(result, "result ! Did the applicant get the job?"); ps.input(argc,argv); ... // here is where the decision is made. ps.output(cout); return 0; }
This is all the code you need to provide all the interface for the example given in the user-interface section. Let's go over it, line by line:
#include <xparam.h>
using namespace xParam;
These two lines will probably appear in all your XParam programs.
ParamSet ps;
This line defines a ParamSet variable. It will store all the parameters we will use in this program, both input and output.
ps << "This program determines whether the applicant will get the job."
All additions to a ParamSet's data are made using operator <<. The simplest addition is that of a string. This string serves as a general description of what the program does, and will appear in the start of the information printed when the program is invoked with the "--help" option (see The User Interface section).
The description string can also be given to the ParamSet as its constructor argument. If several such strings are given to a single ParamSet, they are appended to each other and separated by line breaks when printed.
<< iParamVar(age, "age ! Applicant's age")
As can be seen, operator << can be chained, as usual, when inputting data to a ParamSet object. Beside strings, ParamSets can also accept objects declaring parameters. In XParam, these come in six different flavors. They are "iParamVar", "oParamVar", "ioParamVar", "iParamPtrVar", "oParamPtrVar" and "ioParamPtrVar". All have, more or less, the same format:
First, one must choose which of the six to use. "i" means that this parameter is meant for input only: it will be read when "ps.input()" is invoked, but its value will not be output when "ps.output()" is invoked. "o" is used for output parameters, such as the "result" variable in this case. "io" means that the variable will be both read and written. This is particularly useful for programs that accept information from one program, modify it slightly, and output it for use in another program.
The choice between "ParamVar" and "ParamPtrVar" depends on whether the variable that will be linked with this parameter (which is the first argument given - age in this case) is a pointer type or a non-pointer type. This is very similar to the distinction between "Var" and "PtrVar" that was discussed earlier, and here, too, the distinction could have been avoided, but was kept on purpose. Here, too, the variable can be a class instance, a struct instance, an variable of a built-in type or a variable of an enumerator type.
The second argument, after the name of the variable which will be associated with this parameter, is a string. This string signifies the name of the parameter, as it will be exported to the user. Here, the parameter's name is "age" and the variable it is associated with is also called "age". We recommend that in all cases the parameter's exported name will be the same as the variable it is associated with. It is very intuitive and will save you many mistakes later on.
After the parameter name, there is another, optional part of the string, which begins with an exclamation mark. This is the parameter's description, which will be available to the user when invoking the program with the "--help" option. So, the string can be in either of two forms: "parameter_name" or "parameter_name ! description of the parameter". Legal parameter names are legal C++ variable names: the name must start with an alphabetic character or with an underscore, and all its characters must be alphanumerics or underscores. The description, on the other hand, can be any string.
There is an optional third argument. It is used in the "experience" parameter:
<< iParamVar(experience, "experience ! Years of experience the applicant has",Val(0))
This optional third parameter is the default value. It is presented as a "value source". If no default value is given, the parameter is considered "required" and not entering it will cause error handling to occur. There is no third parameter for output-only parameters.
Here is ParamSet's full interface, including methods which were not discussed so far:
class ParamSet { public: ParamSet(std::ostream& os=std::cout);The default constructor is the most convenient way of initializing a ParamSet. In addition, you can specify the name of an output stream, which will be used by the ParamSet to output replies to a "--help" request. The help-stream can be set and changed after construction using the set_hstream method.
explicit ParamSet(const std::string&);Use this to initialize the general description string.
ParamSet& operator<<(const std::string&);Initialize the general description string if it hasn't been initialized yet, and append to it if it was already initialized. Consecutive strings appended to the general description strings are separated by line breaks.
ParamSet& operator<<(const Handle<Param>& param);The "Handle<Param>" class is the general class handling iParamVar, oParamVar, ioParamVar, iParamPtrVar, oParamPtrVar and ioParamPtrVar parameters.
void set_hstream(std::ostream& os);Set the help-stream used by the ParamSet. The help-stream is the output stream on which the ParamSet outputs replies to a "--help" request. If the help-stream is not set by a call to this method or at construction time, it defaults to the standard output stream (std::cout).
void info(void) const;This method prints a table listing all the usage information of the paramset to the help-stream of the ParamSet. It is the method invoked when the user runs the program with the "--help" option.
void input(std::istream&, bool check=true);ParamSets are not necessarily initialized from the command-line. Use this functionality to input data to the ParamSet from any std::istream. The "check" parameter, defaulting to "true", determines whether after inputting data from the stream the ParamSet will be checked to insure that all input parameters that do not have default values have been initialized. Change the value of "check" to "false" if you intend to input parameters from several sources, and make it "true" only for the last call to input, to make sure that all required parameters received a value.
void input(const int argc, const char* const argv[], bool check=true);This method has the same functionality as the previous one, but accepts an argc/argv like input, instead of using an input stream. This is the most common way to use ParamSets. In addition to what is given by "input(istream)", this method has the added functionality that if the user invokes one of XParam's help-getting options, such as "myprog --help" and "myprog ! int", this routine also provides the requested help on the help-stream of the ParamSet and exits.
void input(const char* const lpstr, bool check=true);This method is equivalent to the previous one, but uses a line parameter string instead of an argc/argv input. A line parameter string is a white-space delimited string, including the entire command line. Under Windows, this is a convenient input format.
void output(std::ostream&=std::cout) const;Use this method to output all output parameters of your ParamSet. The parameters will be output in the same format as they are read, so output from one XParam program can easily be used as input to another XParam program.
void check(void) const;This method checks that no input variable is still without a value, but does this without receiving any more inputs. Using this method after method "input" has the same effect as turning the "check" flag in "input" to true.
MultipleAssignMode multiple_assign(MultipleAssignMode m);This is the first of several methods meant to control the input mode. This particular method accepts either one of three values: FIRST_HOLDS, LAST_HOLDS or IS_ERROR. This value determines how XParam will react if the user attempts to assign a value to the same parameter more than once. You can also use the syntax "paramset << MultipleAssign(mode)" with the same effect. The default value of this option is LAST_HOLDS.
MultipleAssignMode multiple_assign(void) const;This method allows you to query the current mode without changing it.
MatchMode match(MatchMode m);This method lets you control the parameter name-matching algorithm. You can specify EXACT, if you want people to spell out the entire parameter name, or PREFIX, if any unambiguous prefix will do. The PREFIX mode is the default. Note that if any parameter name is the prefix of a different parameter name, a value that is a prefix to both is not considered an ambiguity. The shorter value prevails.
MatchMode match(void) const;Analogously to the multiple_assign directives, one can use this method to query the mode, and can use "paramset << Match(mode)" as an alternate syntax for controlling the mode.
std::vector<std::string> names(void);This is the first of several methods whose purpose it is to let you query the ParamSet about the parameters in it. This particular method returns a list of the names of all parameters stored in the ParamSet.
const Param& operator[](const std::string& name) const;Once you know which names you want to query, you can use operator[] to give you more information regarding the parameter. The next section will detail this information at greater length. Note that this operator is affected by the current mode. If you're using Match(EXACT), the operator will search for a parameter with this exact name. If you're using Match(PREFIX) it will take this to be a prefix, and attempt to complete it. In either case, if the search is unsuccessful, whether because no such parameter exists or because the search resulted in an ambiguity, an exception is thrown.
void import(ParamSet& ps);See section Working with Several ParamSets for a complete explanation of this method.
}; std::ostream& operator<<(std::ostream&, const ParamSet&);Usage of "operator<<" on a ParamSet is equivalent to outputting it to the std::ostream.
std::istream& operator>>(std::istream&, ParamSet&);Usage of "operator>>" on a ParamSet is equivalent to inputting it from the std::istream. Note that this keeps input checking on.
class Param { public: bool is_input() const;Is the parameter an input parameter?
bool is_output() const;Is it an output parameter?
bool was_assigned_to() const;Did the user explicitly assign a value to this parameter?
bool has_default() const;Does this parameter have a default value?
bool has_given_value() const;Did XParam assign a value to this parameter (whether because of a default value or because of an explicit user assign)?
std::string name() const;The parameter's name. This is its full name, even if you found it using a prefix.
std::string description() const;The description string that was assigned to this parameter.
const std::type_info& static_type_info() const;The static type info of the queried parameter's type.
};
This feature can be put to good use, especially in the context of strategy management. The user can specify the desired strategy by choosing a derived class of the base class serving as an interface. The execution line
~/bin> a.out 'url=HTTP("sourceforge.net")'
will work even if the variable connected with "url" is a general "URL*" object. The user chose the protocol HTTP by choosing class HTTP which is, in the example, a derived class from URL. In addition, the XParam interface also allows the user to configure her strategy by supplying variables to the class constructor.
All this would have not been as useful as it is, if XParam hadn't allowed dynamic loading. However, the XParam library allows you to load any class the user specifies at run-time. So, for example, the program connected to the previous execution example may look like this:
#include <xparam.h> class URL { public: virtual void display(void) const=0; virtual ~URL(void) {} }; int main(int argc, char* argv[]) { URL* url; ParamSet ps; ps << iParamPtrVar(url,"url",PtrVal((URL*)NULL)); if (url) { // if "url" wasn't initialized, it would assume the default value, null. url->display(); // display the URL. The program contains no information regarding how this is to be done. } return 0; }
This program has no knowledge of an "HTTP" class, nor of any way of displaying URLs, but the execution line can still work: when necessary, XParam will automatically dynamically link the class library containing the HTTP class and use it. No programmer intervention whatsoever is necessary. (Everything needed to allow this is outside the scope of the C++ program, and will be described in detail in the Registration Interface section.)
Note: some platforms may not have any dynamic loading capability.
In order for XParam to compile properly on these platforms, use the
NO_DYNAMIC_LOADING flag at compile time. XParam does not currently
support dynamic loading in Windows, so you should link your classes
and their registration statically there. Polymorphism is supported
in Windows as usual.
Error Handling
Throughout the operation of XParam, several types of errors can occur.
One is illegal user input given at run-time. (An example of this is
trying to assign a value of incompatible type to a parameter.)
The second is illegal
programmer input which could not have been detected at compile time.
(An example of this is defining two parameters with the same name.)
Finally, there are errors encountered inside user class constructors
and output functions, which are run by XParam.
As for the first two kinds of errors, XParam handles them by throwing an "xParam::Error" exception. This is a class derived from "std::exception" and can likewise give an informative description of the problem using the "what()" method.
As for the third kind of error, the kind encountered in user code, the situation is a little bit trickier. If the user code aborts program execution, there is nothing to be done about it. If, however, the code throws an exception, this exception is caught by XParam, which will then throw its own "xParam::Error" exception class, describing where, exactly, the error was encountered. If the user exception's type is, itself, any derivative of "std::exception", The exception generated by XParam will include in its description (available by the "what()" method) the description of the original exception which was thrown.
As a rule, no XParam function or method will ever throw anything other than an xParam::Error exception.
To conclude, here is the example given in the introductory section once again, but this time, complete with error handling.
#include <iostream> #include <xparam.h> // The XParam library #include "shapes.h" // This is where your shape classes are defined using namespace xParam; using namespace std; int main(int argc, char* argv[]) { Composite my_shape; ParamSet ps; // define a parameter set try { ps << iParamVar(my_shape,"my_shape"); // define variables in the set ps.input(argc,argv); // parse the command line into the set my_shape.draw(); // my_shape is already initialized and ready to use } catch(Error e) { cerr << "Got the following error - " << endl << e.what() << endl; cerr << "Aborting." << endl; } return 0; }
So far, so good. Using abstract interfaces of this sort is commonplace in object-oriented programming. What if, however, the Alien classes needed to be configured? Worse, what if you couldn't know, in advance, what extra information each alien race needs?
Here is a convenient solution:
Class Alien { public: Alien(ParamSet& ps) { set_up(ps); } virtual void set_up(ParamSet& ps) const=0; virtual void initialize(const ParamSet& ps)=0; virtual void Move(void)=0; virtual void Draw(void)=0; };
You give the abstract Alien your ParamSet. In the method set_up the Alien adds "iParamVar" entries into the ParamSet, to indicate what information it needs. Then, after the call to input() in the main program, and after you have read all the information that the main program needs out of the ParamSet, you can return this ParamSet back to the Alien, for it to extract the rest of the information.
This is a good solution. Sometimes, in fact, it is the best solution. However, it does have its drawbacks. First and foremost of these is that the Alien now has access to all your parameters, and can wreak havoc there. Consider, for example, what would happen if the Alien, during set_up changed the input mode from PREFIX to EXACT? The Aliens do not have to be malicious for this to happen: a simple programming oversight can affect your whole program. So, what do we do? This is clearly not the encapsulation we strived for when we first took up a course in object oriented design!
Perhaps you will be more pleased with this, more elegant solution:
Class Alien { public: virtual const ParamSet& set_up(void) const=0; virtual void Move(void)=0; virtual void Draw(void)=0; };
The Alien now manufactures a ParamSet object. This object can have "iParamVar" entries with references to local Alien members, which are not visible from the outside. It doesn't matter. When the Alien has finished constructing the ParamSet, it sends the ParamSet to the main program. In the main program you can now write:
ParamSet ps; ps.import(alien->set_up()); ps.input(argc,argv);
This causes all the parameters defined by the alien to be imported into the global ParamSet. The global ParamSet is not affected by this action in any other way. The user can assign values to the parameters, and these will directly affect the Alien's members.
The Alien's original ParamSet is not required to remain in scope. It can be a local variable. If the Alien keeps the ParamSet, it will be able to query the parameters inside it, as if is was its ParamSet that had executed the input() command, and not the global ParamSet. The Alien does not, however, gain any access to parameters other than its own.
Note that for all this to work, the variables referenced by the Alien's ParamSet must, of course, remain within their definition scope (though they may be invisible). What this means is that keeping references to members of an Alien instance is OK (as long as the Alien is not destructed), but using references to local set_up variables is not. If the referenced objects go out of their definition's scope before the ParamSet goes out of its scope, it will contain dangling pointers.
The advantage of this method is in its simpilicity and the fact that it does
not require you to register new types. In the future, we will provide a
solution that will be adequate for heavy-duty handling of multiple paramsets
as well, and this solution is likely to become deprecated.
Getting Help
XParam offers you two functions in order to get information from your
XParam classes. In a sense, XParam is returning a favor here: after
the registrator (see Registration Interface)
explicitly gave the introspection information of her classes to
XParam, XParam gives you, the programmer, an interface to this
information, for whatever usage you may have for it. Here are the
two functions:
string xparam_name(const type_info& type);
string xparam_help(const string& type_name);
The first function, xparam_name is meant to give you the registered XParam class name for a real C++ class. For example, running xparam_name(typeid(unsigned char)) will return "uchar".
The second function, xparam_help will give you extensive information about a class, including its base types, types derived from it, its constructors, and conversion options to it. The format is self-explanatory. So, if you want introspection information about class duck all you have to do is to run
cout << xparam_help(xparam_name(typeid(duck)));
and all the introspection information will be printed for you.
The function xparam_help is the function that is invoked
when running "my_program ! my_class", discussed in the
Complex Parameter Values
section of The User Interface.
How Pointers Behave in XParam
Though all the relevant information about pointers was mentioned
before, here is a concise summary of the nuances you should be
aware of when using pointers in XParam.
First off, consider the following program segment:
int* i;
ParamSet ps;
ps << ioParamPtrVar(i,"i");
ps.input();
There are several ways in which variable i can be initialized as a result of this code segment. As part of the input, the user can, for example, initialize i by adding an "i=7". The result, in C++ terms, will be equivalent to "i=new int(7)". This is the typical case. You will need to "delete i;" at the end of your program.
The exception to the rule that user initializations cause a call to new is when the user inputs "i=NULL" or one of its variations (e.g. "i=int(NULL)"). In this case, i itself will be assigned the value NULL and not, as in the previous case, *i. No call to new is executed and you do not need to run delete. (Though you can. C++ does not consider deleting a NULL pointer to be an error. The deletion does nothing, but the call does not guarantee that the pointer's value will remain NULL, so do not attempt consecutive deletions.)
However, there is yet a third option of what can happen during the call to "ps.input();". The user may ignore parameter i altogether and not give it a value at all. In this case, XParam will throw an exception, because i has no default value.
If i had had a default value, then not entering a value explicitly would have caused i to be initialized according to the default value given, in a very similar way: a default value of "Val(7)" would have caused an effect equivalent to "i=new int(7);" and a default value of "PtrVal((int*)NULL)" would have caused an effect equivalent to "i=NULL;". Note that the NULL must be cast explicitly to int*, or else XParam will not know how to make your void* value into a legal default value for parameter i.
When using default values, NULL is not the only pointer value you can apply. Any int* value can be used. Consider, for example, this code segment:
int *default=new int(7), *i;
ParamSet ps;
ps << ioParamPtrVar(i,"i",PtrVal(default));
delete default;
ps.output();
delete i;
Here, default can be any valid pointer value. The important things to note about this code segment are:
Here's a common mistake using pointers in XParam: it is tempting to believe that if you enter a non-NULL default value for a pointer variable, then you do not have to check that this pointer is not NULL after calls to XParam. This is not the case. When you run input() the user can still explicitly enter "i=NULL" and this will supersede the default value.
If you really do not want NULL to be an option, and are actually only using pointers instead of integers for your own convenience, then you don't need to use ParamPtrVar at all. Do this instead:
int* i=new int;
ParamSet ps;
ps << ioParamVar(*i,"i",Val(0));
ps.input();
In this example, XParam is completely unaware that the integer it is working on is on the heap. You allocated it and you should deallocate it, but for XParam it is simply an integer and can, of course, not take any NULL or other non-integer values.
This solution will not work, of course, if the reason you're using pointers is to allow polymorphism. Here, XParam should be made aware of the fact that it is dealing with pointers, and therefore a NULL value is a legitimate option.
One last point: how does memory handling work in the presence of exceptions? XParam will only assign deleteable values to your pointers. Therefore, if you're really worried about making your code exception-safe, here's how to do it:
Shape* shape=NULL; ParameterSet ps; try { ps << iParamPtrVar(shape,"shape"); . . . } catch (Error e) { } delete shape;
Because XParam guarantees that any value it assigns to shape will be deleteable, and because shape's original value, NULL, is also deleteable, you can safely delete the pointer, wherever an exception may have occurred, and whether or not an exception did occur.
Next: The Registration Interface
Previous: The User Interface
Up: Table of Contents