Data Types

type_class_t data type type_id_t description
INTEGER uint8 UINT8 8Bit unsinged integer
INTEGER int8 INT8 8Bit signed integer
INTEGER uint16 UINT16 16Bit unsigned integer
INTEGER int16 INT16 16Bit signed integer
INTEGER uint32 UINT32 32Bit unsigned integer
INTEGER int32 INT32 32Bit signed integer
INTEGER uint64 UINT64 64Bit unsigned integer
INTEGER int64 INT64 64Bit signed integer
FLOAT float32 FLOAT32 32Bit IEEE floating point type
FLOAT float64 FLOAT64 64Bit IEEE floating point type
FLOAT float128 FLOAT128 128Bit IEEE floating point type
COMPLEX complex32 COMPLEX32 32Bit IEEE complex float type
COMPLEX complex64 COMPLEX64 64Bit IEEE complex float type
COMPLEX complex128 COMPLEX128 128Bit IEEE complex float type
STRING string STRING string type
BINARY binary BINARY binary type
NONE none NONE none type

An overview of the primitive data types provided by libpnicore.

libpnicore provides a set of data types of well defined size and utility functions related to type management. The basic header file required to use libpnicore s type facilities is

#include <pni/core/types.hpp>

The data types provided by libpnicore include

  • numeric types with all their arithmetic operations
  • string types (currently only one member)
  • and utility types like binary_t, bool_t, and none.

All this types together are refered to as primitive types. The numeric types are ensured to have the same size on each platform and architecture supported by libpnicore. They are mostly aliases to the types defined by the C standard library. However, the utility types binary_t, bool_t, and none are unique to libpnicore and will be explained in more detail in the last sections of this chapter.

Every type in libpnicore is associated with an ID represented by the type_id_t enumeration type. Additionally every type belongs to a particular type class defined by the type_class_t enumeration type. Table~ref{tab:types:basic_types} gives an overview over the primitive types provided by libpnicore and their corresponding type_id_t and type_class_t values.

Compile time type identification

To obtain the ID or class of a type at compile time use the type_id_map or type_class_map type maps.

#include <pni/core/types.hpp>

using namespace pni::core;

//determine the type ID for a given type
type_id_map<float32>::type_id == type_id_t::FLOAT32;

//obtain the class of a particular type
type_class_map<float32>::type_class == type_class_t::FLOAT;

For IDs the other way around is also possible with the id_type_map

#include <pni/core/types.hpp>

using namespace pni::core;

//determine the type for a given ID
id_type_map<type_id_t::FLOAT32>::type data = ...;

For numeric types there are also some other templates for a more detailed type classification

template description
is_integer_type<T>::value true if T is an integer type
is_float_type<T>::value true if T is a floating point type
is_complex_type<T>::value true if T is a complex number
is_numeric_type<T>::value true if T is any of the above types

Identifying types at runtime

data type string representation
uint8 “uint8”, “ui8”
int8 “int8”, “i8”
uint16 “uint16”, “ui16”
int16 “int16”, “i16”
uint32 “uint32”, “ui32”
int32 “int32”, “i32”
uint64 “uint64”, “ui64”
int64 “int64”, “i64”
float32 “float32”, “f32”
float64 “float64”, “f64”
float128 “float128”, “f128”
complex32 “complex32”, “c32”
complex64 “complex64”, “c64”
complex128 “complex128”, “c128”
string “string”, “str”
binary “binary”, “binary”
none “none”

Data types and their string representations.

The recommended way to deal with type information at runtime are the type_id_t enumerations. At some point in time a program might has to determine the type ID of a variable type or of the element type of a container. The basic facility to achieve this is the type_id() function defined in pni/core/type_utils.hpp. The usage of this function is rather simple as shown here

#incldue<pni/core/types.hpp>

using namespace pni::core;

//one could use this with
auto data = get_data(...);

std::cout<<type_id(data)<<std::endl;

The important thing to notice here is that no matter what type the get_data() function returns, type_id() will give you the type ID. In cases where the type ID is given and a classification of the type has to be made four functions are provided where each takes a type ID as its single most argument

function description
is_integer(type_id_t)() returns true if the type ID refers to an integer type
is_float(type_id_t)() returns true if the type ID refers to a float type
is_complex(type_id_t)() returns true if the type ID refers to a complex type
is_numeric(type_id_t)() returns true if the type ID refers to a numeric type

Another important scenario is the situation where a user uses the string representation to tell a program with which type it should work. In such a situation you either want to convert the string representation of a type into a value of type_id_t or vica verse. The library provides two functions for this purpose type_id_from_str() which converts the string representation of a type to a value of type_id_t and str_from_type_id() which performs the opposite operation. The usage of this two guys is again straight forward.

#include <pni/core/types.hpp>
#include <pni/core/type_utils.hpp>

using namespace pni::core;

//get a type id from a string
string rep = "string";
type_id_t id = type_id_from_str("str");

//get a string from a type id
rep = str_from_type_id(type_id_t::FLOAT32);

The binary type

In many cases uninterpreted binary data should be transfered from one location to the other (a typical example would be to copy the content of one file to another). Typically one would use a type alias to something like uint8 to realize such a type. However, this approach has two disadvantages

  • as uint8 is a numeric type with all arithmetic operators available which we do not want for uninterpreted binary data
  • a mere type alias would make uint8 and binary indistinguishable and thus we could not specialize template classes for each of them.

Consequently binary was implemented as a thin wrapper around an appropriately sized integer type with all arithmetic operators stripped away. A short example of how to use binary is the copy_file.cpp example in the examples directory of the source distribution of libpnicore.

In lines $8$ and $10$ we include the pni/core/types.hpp header file and instruct the compiler to use the texttt{pni::core} namespace by default. In line $12$ a vector type with binary elements is defined and an instance of this type is allocated in line $24$. In line $27$ data is read from the input file and stored in the vector. Now, it is clear from here that a vector of type char would have perfectly served the same purpose. The major difference is that unlike char binary has absolutely no semantics. In practice there is nothing much you can do without it rather than store it back to another stream as it is done in line $33$.

The none type

The none type represents the absence of a type. It is a dummy type of very limited functionality and is mainly used internally by libpnicore. One major application of the none type is to do default construction of type erasures (see type_erasures). For all practical purposes this type can be ignored.

The bool_t type

Unlike the C programming language C++ provides a native bool_t type. Unfortunately the C++ standardization committee made some unfortunate decisions with bool and STL containers. std::vector for instance is in most cases specialized for the standard C++ bool type. In the most common STL implementation std::vector is considered an array of individual bits. Meaning that every byte in the vector is storing a total of 8 bool values. Consequently we cannot obtain an address for a particular bit but only for the byte where it is stored. Hence std::vector does not provide the data() method which is required for storage containers used with the mdarray templates (see arrays).

To overcome this problem a new boolean type was included in libpnicore which can be converted to bool but uses a single byte for each boolean value and thus can use the std::vector template. So use the libpnicore bool_t type whenever working with libpnicore templates or whenever the address of a container element is required. For all other purposes the default C++ bool type can be used.

Numeric type conversion

libpnicore provides facilities for save numeric type conversion. These functions are not only used internally by the library they are also available to users. The conversion policy enforced by libpnicore is more strict than that of standard C++. For instance you cannot convert a negative integer to an unsigned integer type. The goal of the conversion rules are set up in order to avoid truncation errors as they would typically occur when using the standard C++ rules.

The basic rule for conversion between two integer type A and B is as follows

A value of type S can only be converted to type B if the value does not exceed the numeric range of type B.

A consequence of this rule is that a signed integer can only be converted to an unsigned type if its value is larger than 0. This is different from the standard C++ rule where the unsigned target type will just overflow.

The second basic rule which governs libpnicore s conversion policy is

During a conversion no information must be lost!

Hence, conversion from a floating point type to an integer type is prohibited as it would most likely lead to truncation and thus a loss of information. Conversion from a scalar float value to a complex value is allowed (as long as the first rule applies to the base type of the complex type) but one cannot convert a complex value to a scalar float type.

Several types cannot be converted to anything than themselves

  • bool_t which can be only the result of a boolean operation.
  • binary as this type is considered to be a completely opaque type conversion to any other type is prohibited. Furthermore no type can be converted to binary.
  • string conversion to string is done exclusively carried out by formatters provided by the IO library.

The library distinguishes between two kinds of type conversion

unchecked conversion
the conversion can be done without checking the value
checked conversion
the value has to be checked if it fits into the target type.

Table~ref{tab:types:unchecked_conversions} gives an overview between which types conversion is possible and whether unchecked or checked conversion will be used.

source / target uint8 uint16 uint32 uint64 int8 int16 int32 int64 float32 float64 float128 complex32 complex64 complex128
uint8 unchecked unchecked unchecked unchecked checked unchecked unchecked unchecked unchecked unchecked unchecked unchecked unchecked unchecked
uint16 checked unchecked unchecked unchecked checked checked unchecked unchecked unchecked unchecked unchecked unchecked unchecked unchecked
uint32 checked checked unchecked unchecked checked checked checked unchecked unchecked unchecked unchecked unchecked unchecked unchecked
uint64 checked checked checked unchecked checked checked checked checked unchecked unchecked unchecked unchecked unchecked unchecked
int8 checked checked checked checked unchecked unchecked unchecked unchecked unchecked unchecked unchecked unchecked unchecked unchecked
int16 checked checked checked checked checked unchecked unchecked unchecked unchecked unchecked unchecked unchecked unchecked unchecked
int32 checked checked checked checked checked checked unchecked unchecked unchecked unchecked unchecked unchecked unchecked unchecked
int64 checked checked checked checked checked checked checked unchecked unchecked unchecked unchecked unchecked unchecked unchecked
float32 none none none none none none none none unchecked unchecked unchecked unchecked unchecked unchecked
float64 none none none none none none none none checked unchecked unchecked checked unchecked unchecked
float128 none none none none none none none none checked checked unchecked checked checked unchecked
complex32 none none none none none none none none none none none unchecked unchecked unchecked
complex64 none none none none none none none none none none none checked unchecked unchecked
complex128 none none none none none none none none none none none checked checked unchecked

Type matrix showing between which types conversion is possible.

The convert() function template

At the heart of libpnicore s type conversion system is the cpp{convert} function template. The declaration of the template looks somehow like this

template<typename ST,typename TT> TT convert(const ST &v);

A value of a particular source type (denoted by the template parameter ST) is passed as an argument to the convert() template. The value of this argument will then be converted to a value of the target type TT and returned from the function template. This function template throws two exceptions

exception reason
type_error in situations where the type conversion is not possible
range_error where the source value does not fit into the target type

The behavior of this function can best be demonstrated examples. .. code-block:: cpp

auto f = convert<float32>(int32(5));

In this example a value of type int32 is successfully converted to a value of type float32, while

auto f = convert<uint16>(float32(-5)); // throws type_error

leads to type_error. According to the conversion policies mentioned above a float value cannot be converted to an integer due to truncation issues.

auto f = convert<uint32>(int32(-3)); //throws range_error

range_error will be thrown as a negative value cannot be converted to an unsigned type. A similar situation would be

auto f = convert<uint8>(int16(10000)); //throws range_error

where range_error would indicate that it is impossible to store a value of 10000 in an 8-Bit unsigned variable.