typeof

This article is intentionally named "typeof" even though formally there is no such keyword in C++. There is simply no better title given the existence of such keyword and its usage in other languages. After all, you want to know what C++ offers in place of it and why.

Why

The reason why C++ has no typeof keyword is mostly historical.

Many compilers did/do have such non-standard extension. Standarizing it would be very troublesome given the differences in existing implementations. The idea is pretty old, before __name was commonly used for extensions so many implementations added a formally-looking keyword, something that today would be very discouraged. The standard would like to avoid breaking existing code or changing the meaning of something that already exists, even if it was already violating the standard.

typeid

This is what you usually want. The keyword works like a function and similarly to sizeof and alignof it accepts both type expressions (typeid(T)) and value expressions (typeid(obj)).

The behavior of the keyword is dependent on exact usage.

  • For typeid(T), if the type T is a reference type, the behavior is identical as if it had no reference. Thus, references are considered to have the same type ID as types they refer to, but pointers have different type IDs. Top-level cv-qualifiers are ignored, thus:

    • typeid(T&) == typeid(T)

    • typeid(const T) == typeid(T)

    • typeid(volatile T) == typeid(T)

    • typeid(T*) != typeid(T)

  • For typeid(obj):

    • If the object is polymorphic (a class that declares or inherits at least 1 virtual function):

      • A runtime cost is incurred to evaluate the exact type of the object (typical implementation will use object's vtable pointer to access global metadata).

      • If the expression is typeid(*ptr) and the pointer is null, additional guarantee exists: instead of undefined behavior an exception is thrown of type std::bad_typeid or a type derived from it.

    • If the object is not polymorphic, the expression is not evaluated and is resolved at compile time (though it's not considered constexpr). Decay does not happen. In this case the resulting type ID may not represent actual (most derived) type of the object.

Similarly to virtual functions in constructors and destructors, using typeid during them will yield IDs about the class currently being constructed/destroyed, not the most-derived one.

In all cases, the resulting expression returns const std::type_info&. The type info object has infinite lifetime (static storage duration strictly technically) and because it's a reference, the actual referenced object can be derived from std::type_info if the implementation needs it.

In all cases, you have to #include <typeinfo> in order to use the keyword. Otherwise the program is ill-formed.

The interface looks as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
namespace std {

class type_info
{
public:
	// you can not create objects of this (and derived) types
	// the only way is to use the typeid keyword
	type_info() = delete;

	// it is unspecified if this is actually called at the end of the program
	virtual ~type_info();

	type_info& operator=(const type_info&) = delete;
	type_info& operator=(type_info&&) = delete;

	// C++11: additionally noexcept
	// C++23: additionally constexpr
	bool operator==(const type_info& rhs) const;
	bool operator!=(const type_info& rhs) const;

	// C++11: all of these are also noexcept
	size_t hash_code() const;
	const char* name() const;
	bool before(const type_info& rhs) const;
};

// helper class to use it as a key in containers
// available since C++11
class type_index
{
public:
	type_index(const type_info& info) noexcept
	: info(&info) {}

	size_t hash_code() const noexcept { return info->hash_code(); }
	const char* name() const noexcept { return info->name(); }

	bool operator==(const type_index& rhs) const noexcept;
	bool operator!=(const type_index& rhs) const noexcept;
	bool operator< (const type_index& rhs) const noexcept;
	bool operator<=(const type_index& rhs) const noexcept;
	bool operator> (const type_index& rhs) const noexcept;
	bool operator>=(const type_index& rhs) const noexcept;
	// C++20
	strong_ordering operator<=>(const type_index& rhs) const noexcept;

private:
	type_info* info;
};

template <>
struct hash<type_index>
{
	size_t operator()(type_index index) const noexcept
	{
		return index.hash_code();
	}
};

}

Hash code

The hash code is the same for all std::type_info referring to the same type. No other guarantees are given, this means that:

  • multiple types may have the same hash (although discouraged by the standard)

  • hash code for the same type can change between invocations of the program

Before

cppreference: returns true if the type of this :cch:`std::type_info` precedes the type of :cch:`rhs` in the implementation's collation order. No guarantees are given; in particular, the collation order can change between the invocations of the same program.

Personally I have never seen this used in practice. std::type_index sounds much better.

Name

You would probably expect the real name of the type, as written in code, with all qualifiers. But this is not the case. The standard places no requirements on it, not even the lifetime of the returned string.

This is an example place where the specification is very tolerant. It's much better to not guarantee something than to label practically-valuable implementations non-conforming.

So what's the reality then? https://en.cppreference.com/w/cpp/types/type_info/name says:

  • Some implementations (such as MSVC, IBM, Oracle) produce a human-readable type name.

  • GCC and Clang, return the mangled name, which is specified by the Itanium C++ ABI

  • The lifetime of the string is tied to the lifetime of RTTI data, which typically lives as long as its originating file (for executables - as long as the program is running, for shared library objects - to the point of unload).

The mangled name can be demangled by:

RTTI

C++ is a language where you don't pay for what you don't use and the existence of the keyword already puts some requirements on implementation - it must provide some metadata in the executable. This metadata is known as RTTI (runtime type information) and compilers offer a -fno-rtti option. For GCC, the option disallows typeid and RTTI-requiring uses of dynamic_cast (typically downcasts). Exceptions are unaffected (any data to support them is generated as needed). Additionally, GCC documents that mixing code compiled with ``-frtti`` with that compiled with ``-fno-rtti`` may not work. For example, programs may fail to link if a class compiled with ``-fno-rtti`` is used as a base for a class compiled with ``-frtti``.

Note that -fno-rtti apart from reducing binary file size, like many options which limit amount of produced metadata will also reduce the possibility of reverse engineering.

Alternatives

RTTI is a costly OOP feature (as far as typical C++ philosophy is concerned). Some frameworks (most notably Qt) generate their own metadata using their own ways.

If you want to use standard-like typeid with a richer API and guarantees, check Boost.TypeIndex library

decltype

This is the second keyword, it bevahes much differently. Instead of returning an object, it behaves like a type name:

1
2
int a = 1;
decltype(a) b = 2;

The keyword has 2 usages:

  • decltype(entity), that is: decltype(ref.member), decltype(ptr->member) and decltype(T::member) - yields the type of the (member) entity, with top-level cv-qualifiers removed

  • decltype(expression) - yields a type depending on value category of the expression:

    • for prvalue, yields T

    • for lvalue, yields T&

    • for xvalue, yields T&&

Because the first point applies only to limited set of expressions, decltype(expression) and decltype((expression)) can yield distinct types (first the type of the entity, second the type of the expression).

1
2
3
4
5
struct S { double x; };
const S* ptr;

decltype(ptr->x) y;       // type of y is double (declared type)
decltype((ptr->x)) z = y; // type of z is const double& (lvalue expression)

Like other operator keywords, it has unevaluated context (the code is not run) and doesn't impose requirements beyond necessary to process the expression (some types in the expression may be incomplete, abstract or have no destructor).

decltype is used almost exclusively in templates, usually to denote function return type or an alias that would otherwise be impossible or complex to express. In non-template code, the need is usually accompanied by an initialization, thus such code typically uses auto.

Since C++14 both keywords can be combined as a single constituent for type declaration: decltype(auto) for initialization or function return type. In such case instead of template type deduction, the resulting type is decltype(expression) where expression is the initializer.

1
2
3
4
5
6
7
8
9
10
11
12
auto x = 1;              // type of x is int - template type deduction
decltype(auto) d1 = x;   // type of d1 is int - decltype(x)
decltype(auto) d2 = (x); // type of d2 is int& - decltype((x))

// auto - perfect_forward will always return by value
// decltype(auto) - perfect_forward return type will be decltype(return_expr)
// (it will return T& if fun returns T& and T&& if fun returns T&&)
template <typename F, typename... Args>
decltype(auto) perfect_forward(F fun, Args&&... args)
{
	return fun(std::forward<Args>(args)...);
}