Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/MicrosoftDocs/cpp-docs/llms.txt

Use this file to discover all available pages before exploring further.

C++ is both a strongly typed and statically typed language: every variable, function parameter, and return value must have a type, and that type never changes after it is declared. Understanding the type system is essential to writing correct and efficient C++ code. This page covers the built-in fundamental types and their sizes in the MSVC implementation, the const and volatile qualifiers, typedef and using for type aliases, the auto keyword for type deduction, decltype for querying expression types, and the implicit and explicit conversions the compiler performs between types.

Fundamental (Built-In) Types

Unlike some languages, C++ has no universal base type. It provides a set of fundamental types built directly into the compiler:
TypeSize (MSVC)Range / Notes
bool1 bytetrue or false
char1 byteASCII characters; signed or unsigned (implementation-defined)
unsigned char1 byteUse for raw byte values
wchar_t2 bytesUTF-16 characters on Windows
short2 bytes−32,768 to 32,767
int4 bytesDefault integral type; −2,147,483,648 to 2,147,483,647
unsigned int4 bytes0 to 4,294,967,295; default for bit flags
long4 bytesSame as int on MSVC (Windows 32/64-bit)
long long8 bytesLarge integer range
float4 bytesSingle-precision floating point
double8 bytesDefault floating-point type
long double8 bytesSame as double on MSVC
int    result      = 0;              // 32-bit signed integer
double coefficient = 10.8;          // 64-bit double precision
bool   isReady     = true;          // boolean
char   initial     = 'A';           // single ASCII character
wchar_t wide       = L'\u03A9';     // Greek Omega (Unicode)
long long bigNum   = 9'000'000'000LL;

// Unsigned types avoid sign-related pitfalls for counts and sizes
unsigned int flags = 0b0001'1010;   // bit flags (C++14 digit separators)
std::size_t  count = vec.size();    // preferred type for sizes and indices
Other C++ implementations may use different sizes for certain numeric types. The C++ standard specifies minimum sizes and relationships (e.g., sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)), but MSVC’s specific sizes above are what you will see in Visual Studio.

The void Type

void is a special type that represents the absence of a value. You cannot declare a void variable, but you can:
  • Declare a function returning void (no return value)
  • Use void* as a generic untyped pointer (discouraged in modern C++)
void printGreeting() {          // function returns nothing
    std::cout << "Hello!\n";
}

void* rawBuffer = malloc(256);  // untyped pointer — use with caution

const and volatile Type Qualifiers

The const qualifier tells the compiler that a variable’s value must not be changed after initialization. Const correctness — using const everywhere it is semantically correct — is an important practice in C++:
const double PI = 3.14159265358979;
// PI = 3.0;   // Error: cannot modify a const variable

const int Max = 100;

// const parameters prevent accidental modification
double circleArea(const double radius) {
    return PI * radius * radius;
}

// const member functions promise not to modify the object
class Counter {
public:
    int value() const { return m_count; }   // const member function
    void increment()  { ++m_count; }
private:
    int m_count = 0;
};
The volatile qualifier tells the compiler that a variable may be modified by means outside the program’s control (hardware registers, signal handlers, another thread without synchronization). The compiler must not cache or reorder accesses to volatile objects:
// Hardware memory-mapped register — must re-read every time
volatile uint32_t* const TIMER_REG = reinterpret_cast<uint32_t*>(0x4000'0000);
uint32_t ticks = *TIMER_REG;  // always reads from hardware address

typedef and using Aliases

Type aliases give a new name to an existing type. The modern using syntax is preferred over the older typedef in C++11 and later because it is clearer and works with templates:
// C-style typedef
typedef unsigned long long uint64;
typedef void (*Callback)(int);     // pointer-to-function typedef

// Modern using alias (preferred)
using uint64    = unsigned long long;
using Callback  = void(*)(int);

// using works with templates; typedef does not
template<typename T>
using Vec = std::vector<T>;

Vec<int>    ints;      // std::vector<int>
Vec<double> doubles;   // std::vector<double>
Prefer using over typedef in all new code. The syntax reads left-to-right like a variable declaration, making complex alias declarations (especially function pointers) much easier to understand.

auto Type Deduction

Introduced in C++11, the auto keyword instructs the compiler to deduce the type of a variable from its initializer. This eliminates verbose type names and makes code more robust to type changes:
// Explicit vs auto declarations
int j = 0;    // explicit type
auto k = 0;   // deduced as int

// auto shines with complex iterator types
map<int, list<string>>::iterator i = m.begin();  // C-style
auto                             i = m.begin();  // modern C++

// auto with lambdas — the only way to name a lambda type
auto multiply = [](double a, double b) { return a * b; };

// auto in range-based for loops
std::vector<std::string> names = {"Alice", "Bob", "Carol"};
for (const auto& name : names) {
    std::cout << name << "\n";
}
auto drops reference and cv-qualifiers from the deduced type. Use auto& or const auto& to preserve them:
int count = 10;
int& countRef = count;

auto   a = countRef;   // deduced as int (reference dropped)
auto&  b = countRef;   // deduced as int& (reference preserved)
const auto& c = count; // deduced as const int&
auto x  = 42;              // int
auto y  = 3.14;            // double
auto z  = "hello";         // const char*
auto s  = std::string{"hi"}; // std::string
auto v  = std::vector<int>{1, 2, 3};
auto fn = [](int n) { return n * 2; };

decltype

decltype queries the type of an expression at compile time without evaluating it. It is especially useful in generic code and trailing return types:
int   x    = 5;
double y   = 3.14;

decltype(x)     a;         // type is int
decltype(y)     b;         // type is double
decltype(x + y) c;         // type is double (arithmetic conversion)

// Trailing return type using decltype
template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
    return t + u;
}

// C++14 simplified: return type deduction
template<typename T, typename U>
auto multiply(T t, U u) {
    return t * u;
}
decltype distinguishes between lvalue and rvalue expressions:
int  n   = 0;
int* ptr = &n;

decltype(n)    // int    (name, not lvalue expression)
decltype((n))  // int&   (parenthesized — lvalue expression)
decltype(*ptr) // int&   (dereference is an lvalue)
decltype(n++)  // int    (post-increment produces a prvalue)
decltype(++n)  // int&   (pre-increment produces an lvalue)

Type Conversions

C++ performs implicit conversions automatically in many contexts, and provides explicit cast operators for the rest.

Implicit (Standard) Conversions

// Arithmetic promotions and conversions
int    i  = 42;
double d  = i;         // int → double: widening, no data loss
float  f  = 3.99f;
int    j  = f;         // float → int: truncation, data loss!

// Pointer conversions
int*   p  = nullptr;   // null pointer constant
void*  vp = p;         // any pointer → void* (implicit)

Explicit Casts (Preferred over C-style casts)

#include <bit>      // for std::bit_cast (C++20)
#include <cstdint>

double pi = 3.14159;

// static_cast: well-defined, checked at compile time
int truncated = static_cast<int>(pi);                    // 3

// std::bit_cast: low-level bit reinterpretation (C++20)
// reinterpret_cast cannot convert between arithmetic types directly
uint64_t bits = std::bit_cast<uint64_t>(pi);             // raw IEEE-754 bits

// const_cast: remove const-ness (use sparingly)
const int ci = 7;
int* mutable_ptr = const_cast<int*>(&ci);

// dynamic_cast: safe downcast with runtime check (requires virtual)
class Base  { public: virtual ~Base() {} };
class Child : public Base { public: void childMethod() {} };

Base*  base  = new Child();
Child* child = dynamic_cast<Child*>(base);  // returns nullptr if fails
if (child) child->childMethod();
Avoid C-style casts like (int)d or int(d) in C++ code. They silently perform whatever combination of static_cast, reinterpret_cast, and const_cast is needed, bypassing the safety checks that the named cast operators provide.

Variable Declarations and Initialization

Always initialize variables at the point of declaration. Uninitialized variables of fundamental types contain garbage values:
int maxValue;           // Not recommended — garbage bits!
int maxValue = 0;       // C-style initialization
int maxValue{0};        // Uniform (brace) initialization — preferred in C++11+
int maxValue(0);        // Direct initialization

// Brace initialization prevents narrowing conversions
double val = 3.14;
int i{val};     // Error: narrowing conversion from double to int
int j = val;    // OK but silently truncates to 3

See Also

Build docs developers (and LLMs) love