Chapter 3 Intègre: basic data types
3.1 Global overview
3.1.1 What is Intègre ?
Intègre is a safe and efficient data types library, designed for
generic algorithm writing. Even if it is now independent from Olena,
it was initially developed to provide value types for images'
pixels. Intègre implements basics types such as integers, floats,
complexes, vectors but also more evolved types such as range (in the
same spirit than Ada) or cycle.
By safe we wean that all operations (arithmetic, assignments, etc.)
are checked. If there is an overflow, the user is noticed, at compile
time if possible, at runtime otherwise.
By efficient we mean that using Intègre additional checks and
features should not decrease too much the overall performances. This
is why Intègre relies intensively on template code and meta
programming.
3.1.2 Interactions with builtin types
Intègre is designed to simplify generic algorithms writing (generic
in the sense of data type). But sometimes, one may want to use builtin
type too in its algorithms, such as int for example. Intègre
provides commodities to accept both Intègre's types and builtin or
external types.
3.1.3 Generic algorithms support
Suppose you want to implement a sum which works on several data types:
template <class DataType>
ResultType Sum(DataType vals[10])
{
ResultType s = zero_for_ResultType();
for (unsigned i = 0; i < 10; ++i)
s += vals[i];
return s;
}
You know that the sum of ten values will certainly not fit into the
same data type, so you would like to store the result into a larger
data type. The same problem stands for the initial value of the
result, 0 is not expressed the same way if we are dealing with
integers, complexes or vectors.
Intègre provides an handy way to solve these problems:
template <class DataType>
ntg_cumul_type(DataType) Sum(DataType vals[10])
{
ntg_cumul_type(DataType) s = ntg_zero_val(DataType);
for (unsigned i = 0; i < 10; ++i)
s += vals[i];
return s;
}
This algorithm will work with almost all Intègre's data types
supporting arithmetic addition, but also with c++ builtin types.
You can refer to the following chapters for more informations.
3.1.4 Available types
Here is a list of available types, you can refer to the type reference
section for more details about each type.
- Reals
-
unsigned integer (int_u)
- signed integer (int_s)
- float with single precision (float_s)
- float with double precision (float_d)
- Enumerated
- Vectorials
-
Static vectors (vec)
- Complexes (cplx)
- Decorators
3.2 Using Intègre
3.2.1 Compilation
Intègre is an active library, and does not provide any object
file. It only provides headers containing generic types and
functions. So one just have to add a compilation flag to make the
compiler find Intègre headers.
All Intègre public services are in the ntg namespace.
Special include files:
-
<ntg/all.hh>: include all Intègre features.
- <ntg/basics.hh>: include all Intègre mecanisms, but
does not include any particular Intègre type.
Other useful include files are mentioned in the documentation of the
concerned features.
3.3 Types properties
To simplify generic algorithms implementation, we need a way to get
types properties by a generic way. Traits are defined to do this. As
they are implemented using traits, it is possible to define properties
for builtin or external types. So one can get properties for non
Intègre native types.
3.3.1 Naming scheme and conventions
Properties associated to types can take two forms: types or values.
3.3.2 Properties which are types
They are suffixed by _type, for example: larger_type,
cumul_type, abstract_type, etc.
Here is the list of common properties:
abstract_type
Empty classes representing the kind of the type. This is useful since
builtin can have associated abstract type. As these classes are
organized by a hierarchical way, their main interest is
static concept checking.
Here are the available abstract types:
ntg_type
Intègre provides a good interaction with builtin types, but
sometimes it is useful to get the Intègre's type associated with a
builtin one (if it exists). For example, ntg_type for
unsigned char is int_u8.
base_type
When using decorators types, such as range or cycle, one
may needs to know the original undecorated type. base_type
represent this type.
storage_type
Intègre's types generally use builtin types to store their
value. For example, int_u8 uses unsigned char to store
its actual value. storage_type represent the type used to store
values.
3.3.3 Properties which are values
Values properties are implemented by static functions, to ensure a
compile time evaluation and to avoid any useless memory usage.
name()
Returns a string with the name of the type. This can be useful
for debugging purposes.
3.3.4 Accessing types properties
To access to the properties describe above, there is two ways, using
directly type_traits or using macros hiding the access.
type_traits<T>
This is the universal way to access properties. For example, use
type_traits<int_u8>::name() or
type_traits<unsigned char>::abstract_type to get the name of
int_u8 and the abstract_type representing the builtin
type unsigned char.
Using type_traits can be rather fastidious (it generally
requires a typename keyword), so macros have been defined to
simplify accesses. You should use type_traits only when it is
not possible to use macros (for example when there is a comma in the
name of the type).
Macros
Macros are defined to every possible property. The naming scheme is
simple: ntg_property_type(Type) is one wants to access to a
type property, ntg_property_val for a value property. Here are
a few examples: ntg_max_val(int_u8),
ntg_abstract_type(float_s), etc.
3.4 Ensuring programs safety
One may want to write an algorithm working only on integers, whatever
its exact type. To accept both builtin types and Intègre types, he
has to write something like that:
template <class T>
void algorithm_on_integer(const T& my_int)
{
// ...
}
There is no way to put a constraints on T since we want to
accept builtin types. A good solution to ensure program integrity is
to insert a structucal check using the ntg_is_a(T, U) macro. It
checks whether the abstract type of T is a subclass of U, and
return a metalic boolean (refer to metalic documentation).
So here the user should write:
template <class T>
void algorithm_on_integer(const T& my_int)
{
ntg_is_a(T, integer)::ensure();
// ...
}
If ntg_abstract_type(T) is not an integer, compilation
will fail with a clear error message.
3.4.2 Overflow checking
Intègre is designed to be a safe data types library. It tries to
make programs safer checking and noticing wrong computations. We want
to prevent implicit side effects, often really difficult to find
out. Here is an example with builtin types:
int i = 256;
unsigned char foo = i; // foo == 0
Another one pointing out arithmetic problems:
unsigned int i = UINT_MAX;
unsigned int j = 5;
unsigned long long k = i + j; // k == 4
These behaviors can be really painful for the programmer, and mostly
are overflow problems. This is why Intègre introduces various
overflow checks for assignments and arithmetic operations.
Sometimes checks have to be dynamic, for example assigning an
int_u16 into an int_u8 may be valid, depending on the
value of the int_u16. This can only be performed at execution
time, when we know the actual value.
Some checks can be avoided however, for example assigning an
int_u8 into an int_u16 is always safe. This is the main
justification of Intègre strong typed paradigm, we want to avoid a
maximal quantity of checks at runtime.
Strong typing and growing types
To keep a maximal amount of static information about variables
range of values, arithmetical operations make types growing. For
example, adding 2 int_u8 returns an int_u9. Growing
rules for each type is detailed in the types
reference section.
This results in the avoidance of a lot of dynamic checks.
Disabling dynamic checks
If you know that your programs works, you can disable dynamic checks
defining the macro NDEBUG at compile time.
Intègre can detect overflow problems. But what should it do when it
detects one ? Here is the reason to live of behaviors.
strict
Stops the program with an error message when a problem is detected.
saturate
Bounds toward the max or the min value of the type,
depending on which is the nearest.
Example: int_u<8, saturate> u = 350; // u == 255.
cycle
Assign the value modulo the value range of the type.
Example: int_u<8, saturate> u = 257; // u == 1.
unsafe
Does nothing. Behaves almost the same way than builtin types. You
should not use this behavior, but it might be useful in a few special
cases.
Intègre types are organized hierarchically: