This is already the fifth blog post in the C++ wonder series. As before, I'll try to provide a mix of information. This time we will have a look at features obtained from C, improved features and useful extensions to the language.
We start with a golden classic: Defining union types.
Unions
A union data type defines useful representation modes for a chunk of memory. For instance we could interpret 8 bytes as a double precision floating point number, a 64-bit integer number, 8 bytes, four UTF-16 characters or any other combination that requires 8 bytes or less.
Of course we could just have 8 raw bytes, which will be converted to one or the other representation. Generally we could work with a void
pointer, which will then be dereferenced and casted. However, why can't the compiler provide all the help we need? In the end, doing two casts and pointer conversions is more work than just writing the assembler code.
This is where a union type comes to help. A union has a number of possible representations and a defined size. The size is basically the size of the largest representation. Let's consider the an example:
union Example {
char characters[4];
int int_number;
float float_number;
};
Here all types have the same size, which makes it trivial to compute the size of the union. We can just pick any type, e.g., the array of raw bytes called characters
, and obtain the size of the union. Let's write a simple program to illustrate what we can do with this union.
int main() {
Example e;
e.int_number = 256;
print(e);
e.float_number = 0.1;
print(e);
}
The print
function will just print the currently stored representation. We get an output that looks like the following:
Integer representation: 256
Float representation: 3.58732e-43
Raw bytes representation: 00000000, 00000001, 00000000, 00000000
Integer representation: 1036831949
Float representation: 0.1
Raw bytes representation: 11001101, 11001100, 11001100, 00111101
This means that by setting any field of an union, we also get different values in the other fields. The behavior is expected, since a union does not store many different fields, but is a single field instead. The members of a union are just (different) representations, which may be useful. However, to fully use a union we often require a flag to indicate, which representation is currently used.
Fortunately C++ inherited enumeration types as defined in C. Even better, C++11 improved such enumerations a lot.
Enumeration classes
An enumeration is just a collection of constants. If we do not provide a (numeric) value, then the compiler will choose one for us, starting with zero. An example could be as simple as the following snippet:
enum cardsuit {
CLUBS = 1,
DIAMONDS = 2,
HEARTS = 4,
SPADES = 8
};
In practice it was already a giant step forward if enumerations have been favored over macros / constant defines. Therefore the previous C++ standard was still quite happy with the current state of the enumeration type, even though values may appear untyped in the code.
That means that we could use CLUBS
or DIAMONDS
without specifying the enumeration kind, cardsuit
. The only constraint one could create, is a variable of type enum cardsuit
. Even though, that worked in general, it was no restriction on setting the value from plain integers or other enumerations. Therefore programming mistakes have been a certainty. The main issue was that C exposes the integer representation of enumeration values directly. Additionally all arithmetic operations on enumeration values are permitted.
C++11 introduces a new type, to keep backward compatibility. The new type is called enumeration class and represents a type-safe alternative to classic enumerations.
enum class CardSuit {
Clubs = 1,
Diamonds = 2,
Hearts = 4,
Spades = 8
};
We can now use these values by referencing CardSuit::Clubs
or other values using the namespace resolution operator (::
) on the enumeration type, e.g., CardSuit
. This way we are constraint to values of the provided enumeration. Of course some standard cast operators are already overloaded, to provide us with the ability to transform elementary types to our enumeration and vice-versa.
Another interesting possibility is the ability to define the size of the enumeration. We are no longer limited to accept the compiler's choice (which is the minimum number of bytes to carry all values), but we can define what kind of elementary data type forms the basis for our enumeration.
If we want to just use a single byte, with a signed 8-bit integer representing the data, then the following would work fine.
enum class Colors : std::int8_t {
Red = 1,
Green = 2,
Blue = 3
};
In general we should all leave the classic enumeration type behind us. The class enumeration is far more flexible and (type) safe. It is not a coincidence that the design is quite similar to the enumeration type in C#.
Variadic templates
Templates are one of the biggest success factors of C++. Otherwise the name C with classes would have been more appropriate. However, even though they are really powerful (compared to, e.g., generics) and super useful, they could be better. Especially meta programming capabilities like templates cry for greater flexibility. Before C++11 templates have been constraint to the number of named arguments.
However, in general we do not want to be limited. C# would now solve such a problem by introducing an array of types (likely with a params
keyword), but this is not the design of C++. Here we can rely on our favorite operator: ...
, called ellipsis operator.
It should not come as a surprise that variadic templates have been introduced in D, since D is a very strong language for meta programming. Let's see a simple example:
template<typename... Values>
class tuple;
Here we may pass in 0 to arbitrary many type arguments. The ellipsis operator is in front of the name - this is important. When it occurs to the left of the name of a parameter, it declares a parameter pack. Using the parameter pack, we can bind zero or more arguments to the variadic template parameters. By contrast, when the ellipsis operator occurs to the right of a template or function call argument, it unpacks the parameter packs into separate arguments.
There are two things we can do with a variadic template code: Either pass it the variadic (type) arguments on to other code, that is based on variadic template code, or evaluating it recursively.
This may seem odd in the beginning, but there are no special functions or calls that may give us information on the current template argument usage. D has such compiler function calls, which will result in cleaner code, but this is not the philosophy of C++. Here we will be required to construct each call.
Let's see an example:
// termination version
void foo() {
// ... Just a dummy (usually)
}
template<typename TArg, typename... TArgs>
void foo(const TArg& arg, const TArgs&... args) {
// ... Do something with arg
func(args...); // ... Again with the rest
}
The version provided above is quite a generic recipe. We provide a version of the function with no arguments and one with the least amount of arguments. Finally we also have add the overload with a single type argument and the variadic argument. Then we start decrementing the variadic arguments by calling the function recursively.
Type aliases
Type aliases exist in C since the dawn of time. The typedef
declaration is useful in a lot of situations. In C elementary types are not constraint to a single identifier. Instead they might composed of a free identifier, together with parts like signed
, unsigned
, struct
etc.
A typical example would be the following code:
typedef unsigned char byte;
Here we call an unsigned character type just byte
(like in C#). The alias is, however, restricted to a single word and cannot be composed with keywords such as signed
. These keywords are limited to elementary data types and cannot be applied on aliases of elementary data types.
However, one drawback exists: We cannot template typedef
aliases. Since templating is a C++ feature, we are left on our own.
C++11 brings in a new construct to break this limitation. Here we rely on the using
keyword, as presented in the previous C++ wonders post. It has the same semantics as if it were introduced by the typedef
specifier..
An example is worth more than words:
// template type alias
template<class T>
using ptr = T*;
// the name ptr is now an alias for pointer to T, i.e. T*
ptr<int> x;
This allows to define a whole range of useful type aliases. The new using
construct is not restricted to template arguments and should therefore be preferred to the typedef
declarations. More C++, more powerful and more consistent.
Alignment of variables
C++ is an amazing language. It is quite close to the metal, but provides enough abstraction mechanisms to unlock the potential of the programmer. Sometimes, however, we require high performance in our code. In C++ all the possibilities of the machine can be accessed. Nevertheless, most of these operations can only be accessed due to intrinsics or inline assembler.
In the end, however, we want to be portable and independent of a particular compiler. Even though, this might be hard to achieve for a certain configuration or architecture, it is still possible. Some features are helping as a lot. One of these features is the alignment operator of C++11.
Alignment is a restriction on which memory positions a value's first byte can be stored. This position sometimes should be of a particular offset (in bytes) in order to guarantee fast execution. In general, the position has to do with number of bytes per cache line. We want to load cache lines, which are effectively closed, such that we do not have to load another cache line to finish the current operation.
For instance an alignment of 32 means that the memory address is a multiple of 32. A simple would look as follows:
#include <cstdlib>
int main() {
alignas(16) int a[4];
alignas(1024) int b[4];
printf("%p\n", a); // e.g. 0xbfa493e0
printf("%p", b); // e.g. 0xbfa49000
return 0;
}
The number of zeros basically tell us the offset of the variable in memory. For 16 we have at least one zero, for 1024 at least 2. An alignment of 4096 would feature at least 3 zeros.
Additionally we do not only have the alignas
, but also the alignof
operator. While the first one sets a variable with the correct offset, the second one checks if the offset is correct, in the most efficient and safest manner.
int a[4];
assert(alignof(b) == 1024);// best way to check for correct alignment
This way more features that had to be implemented using intrinsics, special library functions or inline assembler, can be moved to the language itself. This is only the start right now, but it is promising. The ultimate goal is to have a language that makes writing fast, robust, reliable and portable code possible.