C++ Type Generator


What is C++ Type Generator?
Hello World!
Generator usage and options
Type description commands
Controlled conversions between unrelated types
Syntax issues
Performance of the generated code

Download typegen:

save
typegen-1.1.0.tar.gz (7 kB)
save
typegen-1.1.0.zip (8 kB)

See changelog for the list of improvements.


What is C++ Type Generator?

The C++ Type Generator is a very simple program generating classes that can play the role of value types that have characteristics according to type descriptions provided by the user.
The purpose of the generator is to promote good software engineering practice by making it much easier to think in terms of domains and types with constraints and by providing extensive assistance to the programmer in terms of finding bugs related to unsafe usage of fundamental types in C++.
The assistive abilities of the generated types include:

In other words, the C++ Type Generator is supposed to help programmers achieve high level of type safety in their C++ programs.
It is supposed to compete with approaches based on templates and metaprogramming in the following ways:

The generator itself is a simple script that works by interpreting the type definition file provided by user and creating the C++ header file with definitions of new types.

Hello World!

# the following is in mytypes.tcl:
type string Greetings
type number Distance
type number Velocity
type number Duration
operation Velocity * Duration -> Distance

// and the following is in hello.cpp:
#include "mytypes.h"
#include <iostream>
using namespace std;

int main()
{
    Greetings msg = "Hello!";
    cout << msg << '\n';

    cout << "Enter velocity: ";
    Velocity v;
    cin >> v;

    cout << "Enter duration: ";
    Duration t;
    cin >> t;

    Distance d = v * t;
    cout << "The distance is: " << d << '\n';
}

The whole project can be compiled in the following way:

$ tclsh typegen.tcl mytypes.tcl
$ g++ hello.cpp

The type-safety gain in this code is based on the fact that the defined types are really distinct from each other and there is only one operation that allows to perform conversions between the three numeric types.

Generator usage and options

The generator itself is implemented in the typegen.tcl file, which is a Tcl script. It can be invoked as:

$ tclsh typegen.tcl definition_file_name [option value option value ...]

The definition_file_name is a name of the file that contains definitions of types provided by user.
The generated file has the same root name as the definition file, but with the .h suffix.

All options are boolean variables and their values need to be provided after each option name.
Currently the following options are recognized:

option meaning
-rawBaseTypes If the value is true, the generator will not generate class wrappers for defined types, but only simple typedefs that create aliases to the underlying types. This setting basically switches off all the type safety that could be otherwise gained, but might be useful for example for final "release" builds, where the correctness of the program was already verified and the possible gain in performance justifies the loss of safety belts.

Note: apart from removing all safety belts, this option can influence the semantics of types with wrapped or saturated constraints. Use it with care.

The default value for this option is false.
Example:

$ tclsh typegen.tcl mytypes.tcl -rawBaseTypes true
-withTypeInfo If the value is true, the generator will generate for each number type the additional class named TypeName_type which contains the typedef for the base type of the given type with additional begin() and end() static member functions if the type has range constraint.

The default value for this option is true.
Example:

$ tclsh typegen.tcl mytypes.tcl -withTypeInfo false
-checkUninitialized If the value is true, the generated types assert that the value is already initialized before being read.
The default value for this option is true.
-includeString If the value is true, the generated header file contains the #include <string> directive. Setting it to false makes sense only when the string types are not used at all or they use something different than std::string as their base type.
The default value for this option is true.
-includeStdExcept If the value is true, the generated header file contains the #include <stdexcept> directive. Setting it to false makes sense when none of the types uses exceptions for reporting constraint violation conditions.
The default value for this option is true.
-withIOStreams If the value is true, the generated header file contains the #include <istream> and #include <ostream> directives and the types have insertions and extractions operations for standard streams. Setting this option to false will suppress the generation of these operations.
The default value for this option is true.
-withDomainCast If the value is true, the generated header file contains the domain_cast template function provided specifically for explicit conversions between generated types.
The default value for this option is true.
-withFastSubtypes If the value is true, the implicit conversions from the subtype to its supertype bypasses the range checking of the supertype.
The default value for this option is true.

Type description commands

The following commands are provided for type generation and can be used in the type definition file:

command meaning
literal This is the basic command that forces the generator to inject the given string in its output.

Example:

literal "\#include \"MyBigNumber.h\""
include This command is a helper for inserting #include directives.

Example:

include MyBigNumber.h
include <some/system/header.h>
type number This command defines new numeric type. In its simplest form it takes one argument, which is the name of the new type.

Example:

type number MyNumber

Possible additional options (always after the name of the type) are:

option meaning
-range The value of this option must be either an empty list (which is the default) or the list of two values denoting the range constraint for this type. `

Example:

type number MyType -range {0 100}
-init The value of this option defines the default value for the given type. This option has "empty" default.

Example:

type number MyType -init 0
type number MyRange -range {100 200} -init 100
-withDefaultConstructor Specifies whether the default initialization is allowed for the given type. By default it is true.
Note: it is an error to specify -withDefaultConstructor false and some -init value at the same time.

Example:

type number MyType -withDefaultConstructor false
-constraintMode Defines the behaviour of the type in the case the range constraint is violated. It is ignored if there is no range.
The following values are recognized:
  • abort - the program is aborted (default)
  • exception - the std::range_error exception is thrown
  • saturate - the value is modified to be within the given range, and is adjusted to either lower (if it was too low) or upper (if it was too high) limit of the range
  • wrap - the value is wrapped, so that exceeding the upper limit causes it to "come back" from the lower side - same the other way round (note: this does not currently work with floating-point types)
  • none - the value is allowed, even if it is out of the given range.

Example:

type number MyType -range {-100 100} -constraintMode saturate
-use Specifies the underlying type that is used to keep the actual value. By default it is int.

Example:

type number MyType -use double
-subtypeOf The value must be either an empty list (default) or the list of names of types that were already defined.
The effect of this option is to allow implicit conversions to the supertypes (direct and indirect) of the given type.

Example:

type number MyType -subtypeOf MyOtherType
type number A -subtypeOf {B C}
-operations The list of operations that are allowed for this type.
The following values are recognized in the list:
  • equ - the type has equality (==, !=) operators
  • rel - the type has relational (<, >, <=, >=) operators
  • inc - the type has increment (pre- and post-) operators
  • dec - the type has decrement operators
  • add - the type has addition operations (+=, -=, +, -) that can be used with its base type only
  • mul - the type has multiplication operations (*=, /=, *, /) that can be used with its base type only and without the possibility to divide base type value by the given type
The default value for this parameter is {equ}.

Example:

type number MyLoopIndexType -operations {equ inc}
-rawBaseType The meaning of this option is the same as of the global -rawBaseTypes in the sense that it overrides it locally (for the given type only).

Example:

type number MyType -use double -rawBaseType true
-withTypeInfo Same as global -withTypeInfo, locally overrides it.
-withIOStreams Same as global -withIOStreams, locally overrides it.
-comment Specifies the literal one-line comment that will be automatically added (with "// ") just before the generated type.

Example:

type number MyType -comment "Very special type."

For example, with all options having their default values, the result of this definition:

type number MyType -range {0 100}

is the following code in the generated header file (some definitions and bodies are not shown here):

class MyType_type;
class MyType
{
public:
    typedef MyType_type info_type;
    typedef int base_type;

    MyType(base_type v);

    MyType();

    MyType(MyType const & other);

    void operator=(MyType const & other);
    void operator=(base_type v);

    base_type get_value() const;

    bool operator==(MyType const & other) const;
    bool operator!=(MyType const & other) const;

private:
    base_type value_;
};

class MyType_type
{
public:
    typedef int base_type;

    static MyType begin() { return MyType(0);  }
    static MyType end()  { return MyType(100); }
};

std::ostream & operator<<(std::ostream & os, MyType const & v);
std::istream & operator>>(std::istream & is, MyType & v);
type string This command defines new string type. In its simplest form it takes one argument, which is the name of the new type.

Example:

type string MyString

Possible additional options (always after the name of the type) are:

option meaning
-use Defines the underlying type that is used to keep the actual value. By default it is std::string.

Example:

type string MyString -use my::better_string
-operations The list of operations that are allowed for this type.
The only values (and all included by default) that are currently recognized are:
  • equ - the type has equality (==, !=) operators
  • rel - the type has relational (<, >, <=, >=) operators
-withIOStreams Same as global -withIOStreams, locally overrides it.
-comment Specifies the literal one-line comment that will be automatically added (with "// ") just before the generated type.

Example:

type string MyString -comment "Very special string type."

For example, with all options having their default values, the result of this definition:

type string MyString

is the following code in the generated header file (some definitions and bodies are not shown here):

class MyString
{
public:
    typedef std::string base_type;

    base_type value;

    MyString(char const * str);
    MyString(base_type const & v);

    MyString();

    MyString(MyString const & other);

    void operator=(MyString const & other);
    void operator=(base_type const & v);
    void operator=(char const * str);

    bool operator==(MyString const & other) const;
    bool operator!=(MyString const & other) const;

    base_type const & get_value() const;
};

std::ostream & operator<<(std::ostream & os, MyString const & v);
std::istream & operator>>(std::istream & is, MyString & v);

Note that in this class there is a public member value. The reason for this is that different string classes tend to have different interfaces (and this is what makes them unlike the numeric types, where it is clear what is the common-sense set of operations supported by the base type) and it is not possible - in general - to provide the adaptor that would be universal. The public member value allows unconstrained access to the underlying string value, whatever its base type.
If you use only one string class as an underlying type, feel free to modify the generator to produce more elaborated adaptor.

operation This command defines the binary operation that relates three types, where at least one of the arguments is a generated type.
Any of the binary operators is allowed, if it is supported by the underlying types of the given types.

Example:

operation Velocity * Duration -> Distance

The result of the above command is the following generated code:

inline Distance operator*(Velocity const & left, Duration const & right)
{
    return left.get_value() * right.get_value();
}

If neither of the arguments is a generated type, then this command is ignored.

insideNamespace This command allows to define types within regular C++ namespaces, but unlike the literal "namespace ..." command, it provides the notion of the namespace to the generator, allowing proper generation of other related code.
Namespaces can be nested or re-opened.

Example:

insideNamespace MyNamespace {
    type number MyNumber
}

As a result, the MyNumber will be generated in the MyNamespace namespace, so that the fully qualified name will be MyNamespace::MyNumber. The fully qualified names should be also used in relevant -subtypeOf listings.
More elaborated example (which is part of the test suite):

insideNamespace N1 {
   type number U
   insideNamespace N2 {
       type number V
   }
}
insideNamespace N3{
   type number X -subtypeOf N1::N2::V
   insideNamespace N4 {
       type number Y -subtypeOf N1::U
   }
}

Controlled conversions between unrelated types

In order to allow controlled conversions between unrelated types, the domain_cast operation is provided, which syntax is the same as for other C++ casts.
If the source type is not generated, the domain_cast is equivalent to static_cast.
Example:

With the following type definitions:

type number A
type number B -subtypeOf A
type number C

the following C++ code is legal:

A a = ...;
B b = ...;
C c = ...;

// B is a subtype of A, so this operation is legal implicitly:
a = b;

// this is not necessary, but still possible:
a = domain_cast<A>(b);

// and the other way round and with unrelated types it is always:
b = domain_cast<B>(a);
a = domain_cast<A>(c);

Hint: the domain_cast is as ugly as other C++ casts and is therefore easy to find in source code.

Syntax issues

The whole "type definition language" is implemented in Tcl and is nothing more than a set of Tcl commands - and the type definition file is nothing more than a regular Tcl program.
Those programmers who already know Tcl are less likely to run into problems with Tcl syntax (notably some quoting issues, things related to line breaking and curly braces and so on) and in addition can enjoy other Tcl features in their type definitions, like:

set PI 3.1415926
type number Angle -use double -range [list -$PI $PI]
type number MyRange -range [getTheRangeFromExternalDatabase]

or anything else (like source'ing other files, etc.) that they find useful.

For those programmers who don't know Tcl the only reasonable recommendation is: Learn it. :-)

Performance of the generated code

For performance reasons, all functions are generated as inline. Modern compilers should have no problems optimizing them to the level that is equivalent to the use of fundamental types directly.
If the aggressive optimization options cannot be used and the ultimate performance is a goal, the following hints may prove useful with regard to the numeric types:

Note: Both options above remove the safety belts which undermines the whole idea of this code generator. The use of these options is justified only after the programmer had already gained the confidence that the code is correct and well tested. Compiling and testing the program with and without these options (kind of debug/release builds) might be a justified strategy, but it is to be reminded that wrapping and saturation for numeric types will obviously lose their semantics when the type is just typedefed to the underlying base type.