New Age C++

C++ Introspection

C++ has several methods -- including the <type_traits> header and runtime type information -- to help your application make type-based decisions.

C++ programmers, unlike Java and .NET developers, don't get the benefit of runtime introspection -- the ability to learn about a class's features. However, C++ offers some limited introspection at both compile time and runtime that are worth examining.

The <type_traits> Header
I'll start with compile time. C++11 added a header, <type_traits>, with functions that can ask aspects about a type. This makes sense with template programming (and meta-programming as well), because you're working with generic types; but you still want to make sure the types meet certain premises.

<type_traits> is less granular than Java and .NET introspection APIs, because it doesn't allow you to extract class information via reflection (like method names or its parameters). Its functions help determine things like whether a type is C-compatible (plain-old data), or a class, function, reference, void and so on. Here are a few examples of <type_traits>:

// #include <type_traits>
is_pod<my_type>::value // The expression is true if my_type is a plain
                       // old data type compatible with C
is_array<my_array>::value // True if my_array is a primitive array

is_abstract<my_type>::value // True if my_type declares or inherits
                            // pure virtual functions
is_base_of<my_type_1, my_type_2> // True if my_type_2 derives from
                                 // my_type_1
<type_traits> also helps determine if a type supports certain operators like the default constructor, copy and move assignments, and so on:
is_default_constructible<my_type>::value // The expression is true if
                                         // my_type has an accessible default constructor
is_move_assignable<my_type> // True if my_type has an accessible move
                            // assignment operator
is_destructible<my_type> // True if my_type has a non-deleted destructor
Compile-time assertion checking is a great complement to the functions included in <type_traits>. This is probably why they came together in C++11. The reserved keyword static_assert evaluates a constant expression, usually based on <type_traits> functions (although this isn't mandatory). If the expression is false, a compile error is issued:
static_assert(has_virtual_destructor<my_type>::value,
  "my_type needs a virtual destructor for derived classes to be safely deleted.");

Compile-Time Type Inference
The C++ equivalent of the C# keyword var is auto. This reserved word existed previously, but it was repurposed in C++11. Before, it meant automatic storage duration. In practice, an automatic variable is allocated at the beginning of the enclosing block in which it's declared, and de-allocated at the end. With C++11, a variable declared as auto means to declare that the variable is the type of the expression used to initialize it. In other words, an automatic variable must be initialized or a compile error will be produced:

auto i = 0; // i is int
This keyword is extremely practical, even in cases when you don't need to infer a type, and you just want to avoid typing it:
list<string> hello_list {"hello", "world"};
auto hello_iter = begin(hello_list); // hello_iter type is list<string>::iterator
Auto comes with a complementary new keyword -- decltype -- that declares a variable by inferring its type, but without getting it initialized:
template<class T, class U>
auto add(T t, U u) -> decltype(t + u) // The return type of add is the type of operator+(T,U)
{
  return t + u;
}
// add(1, 1.2) type is double
That covers compile-time introspection. What about runtime?

Runtime Type Information
Runtime introspection, curiously, was around before C++11. Like <type_traits>, it's limited to type names, not class members. Rather than generic programming, runtime type information (RTTI) is useful in polymorphic scenarios when you need to know what type of hierarchy is being pointed to or referred:

// #include <typeinfo>
auto my_array = { "Hello,", "world!" }; // std::initializer_list<char*>
cout << "The type of my_array is " << typeid(my_array).name() << '\n';

Make C++ Types Speak by Themselves
The principle of object orientation is all about abstraction. Consequently, algorithms that act on types should defer to these types to deal with their internals in their way. However, this might be sometimes unavoidable. In generic programming, for instance, you may need to keep some control over the kind of specialization made on a template. The functions declared in the <type_traits> header, especially when combined with static assertions, are effective tools you can use to make decisions at compile time. Likewise, when you need to know the actual type referred to by a pointer or reference, RTTI can assist you.

This level of introspection doesn't equal what you get with managed code, but it can certainly help your application make decisions based on actual instantiated types.

About the Author

Diego Dagum is a software architect and developer with more than 20 years of experience. He can be reached at [email protected].

comments powered by Disqus

Featured

Subscribe on YouTube