When writing code, you might want to verify what type the compiler deduced for a variable. Modern IDEs like Visual Studio and VS Code provide this capability directly. Simply hover your mouse over the variable in question, and a tooltip will display its type. For this to work, your code should be syntactcially correct since the IDE performs static analysis to determine types.
int value;
auto result = value;
auto& reference = value;
Hovering over result will show "int result", while hovering over reference will display "int& reference". While IDEs work well with basic C++ types, they may struggle with more complex types or in complicated code contexts.
Using External Tools
When IDEs fail to display types correctly, external tools can help. Cpp Insights (https://cppinsights.io) is an excellent online tool based on Clang that transforms your C++ code into its final form. Similar to how preprocessors expand macros, this tool goes further by converting range-based loops, structured bindings, default constructors, initializer lists, and auto/decltype declarations into their explicit types. It's particularly valuable for debugging as it shows template instantiation code.
The interface displays your original code on the left and the transformed version on the right after clicking the triangle button. The right panel shows type aliases converted to their concrete types. You can select different C++ standards from the dropdown menu. Note that the tool may have occasional bugs, such as incorrectly displaying function types as their return types in some cases.
Compile-Time Type Printing
The compiler always knows the exact types, but we need a way to extract this information. One technique is to intentionally trigger a compilation error that will reveal the type. We can declare an incomplete template class:
template<typename T>
class TypeInspector;
Since this template has no definition, attempting to instantiate it will cause a compilation error. We can use this to inspect any varialbe's type by passing it as a template parameter:
const int number = 42;
auto deduced = number;
TypeInspector<decltype(deduced)> instance;
The compiler will generate an error message containing the type:
error: implicit instantiation of undefined template 'TypeInspector<int>'
The type appears inside the angle brackets after the template name. However, this approach only handles one type at a time. We can improve it using C++11's variadic templates:
template<typename... Types>
class TypeInspector;
Now we can inspect multiple types simultaneously:
template<typename... Types>
class TypeInspector;
int calculate(int, int) {
int result;
return result;
}
class Data {
public:
int value = 0;
};
int main() {
const Data obj;
const int constant = 1;
auto var1 = constant;
using T1 = decltype(var1);
using T2 = decltype((var1));
using T3 = decltype(obj.value);
using T4 = decltype((obj.value));
using T5 = decltype(calculate);
TypeInspector<T1, T2, T3, T4, T5> instance;
}
The resulting error message will show all types:
error: implicit instantiation of undefined template
'TypeInspector<int, int &, int, const int &, int (int, int)>'
Runtime Type Output
Sometimes we need to output type information during program execution. C++'s RTTI (Run-Time Type Information) provides the typeid operator and type_info class. Applying typeid to a variable or type returns a type_info object with a name() member function that returns a C-style string representation of the type:
auto add(auto a, auto b) { return a + b; };
auto sum = add(1, 2.0);
printf("Type of sum: %s\n", typeid(sum).name());
auto text = add("hello"s, "world"s);
printf("Type of text: %s\n", typeid(text).name());
The output might look like:
Type of sum: d
Type of text: NSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE
Here, 'd' represents double (int would be 'i'), and the string type shows a mangled name. This approach has limitations, especially with reference types:
int num = 5;
auto& ref = num;
printf("Type of ref: %s\n", typeid(ref).name());
Despite ref being of type int&, the output is simply 'i' (int). The reference qualifier is lost, and const/volatile qualifiers are also ignored.
A more robust approach uses template functions with compiler-specific macros that contain function signatures, including parameter types. These macros are compiler extensions: __PRETTY_FUNCTION__ in GCC/Clang and __FUNCSIG__ in MSVC:
#include <iostream>
template<typename... Types>
void ShowTypes() {
// For GCC/Clang
std::cout << __PRETTY_FUNCTION__ << std::endl;
// For MSVC
//std::cout << __FUNCSIG__ << std::endl;
};
int compute(int, int) {
int result;
return result;
}
class Container {
public:
int element = 0;
};
int main() {
const Container obj;
const int fixed = 1;
auto var1 = fixed;
using T1 = decltype(var1);
using T2 = decltype((var1));
using T3 = decltype(obj.element);
using T4 = decltype((obj.element));
using T5 = decltype(compute);
ShowTypes<T1, T2, T3, T4, T5>();
}
The output varies by compiler:
// Clang
void ShowTypes() [Types = <int, int &, int, const int &, int (int, int)>]
// GCC
void ShowTypes() [with Types = {int, int&, int, const int&, int(int, int)}]
// MSVC
void __cdecl ShowTypes<int,int&,int,const int&,int(int,int)>(void)