Skip to content

Basic Syntax & Types

In C++, understanding the core type system and variable semantics is crucial for writing efficient and safe code. Because C++ is a strongly-typed and statically-typed language, the compiler must know the type of every entity at compile time.


1. Comments and Code Documentation

Comments are ignored by the compiler but are essential for humans reading the codebase. Modern C++ codebases typically use two comment formats:

1
2
3
4
5
6
// This is a single-line comment. Used for brief notes.

/*
   This is a multi-line comment block.
   Use this for longer explanations.
*/

Doxygen Documentation Style

For professional C++ libraries, comments are often parsed by documentation generators like Doxygen. Use three slashes /// or a block starting with /** to generate API documentation:

1
2
3
4
/// Read data from a digital GPIO pin.
/// @param pin The GPIO pin number (BCM layout).
/// @return true if the pin reads HIGH, false if LOW.
bool readGpioPin(int pin);

2. Variables and Modern Initialization

A variable is a named memory location. Modern C++ (C++11 and later) introduced Brace Initialization (also called Uniform Initialization) to unify initialization syntax across all types.

Initialization Styles in C++

1
2
3
4
int a = 10;     // 1. Copy initialization (C-style)
int b(10);      // 2. Direct initialization (Constructor-style)
int c{10};      // 3. Direct brace initialization (Modern)
int d = {10};   // 4. Copy brace initialization

Why You Should Prefer Brace Initialization {}

  1. Prevents Narrowing Conversions: The compiler will raise an error if you attempt to assign a value that cannot fit into the target type without loss of precision.
    int x = 5.5;    // Compiles, but x becomes 5 (truncation warning).
    // int y{5.5};  // Compile Error! Narrowing double to int is forbidden.
    
  2. Avoids the "Most Vexing Parse": Sometimes, standard parentheses syntax can be mistaken by the compiler as a function declaration rather than object initialization. Brace syntax avoids this confusion.
    1
    2
    3
    4
    5
    // Supposed to call default constructor, but interpreted as function declaration:
    // Widget w(); 
    
    // Correctly initializes a Widget object:
    Widget w{}; 
    

3. Type Deduction (auto)

The auto keyword instructs the compiler to automatically deduce the type of a variable from its initializer expression. This is resolved at compile time, meaning there is zero runtime overhead.

1
2
3
auto x = 5;          // Deduce as int
auto y = 3.14f;      // Deduce as float
auto z = "RPi";      // Deduce as const char*

Important Gotcha: CV Qualifiers and References

By default, auto strips away const and reference qualifications (&).

1
2
3
4
const int active_pin = 22;
auto pin_copy = active_pin; // Type is "int" (const is dropped!)

const auto& pin_ref = active_pin; // Type is "const int&" (explicitly preserved)

Rule of Thumb: - Use auto for primitive types when the type is obvious. - Use const auto& for complex objects (like classes and STL containers) to prevent expensive copy operations.


4. Fundamental Types and Platform Architecture

C++ provides native types whose sizes are implementation-dependent. When programming on a Raspberry Pi, the word size depends on whether you run a 32-bit (ARMhf) or 64-bit (ARM64) OS.

Standard Integer Types

Type Typical Size (ARM64 / 64-bit OS) Range (Signed) Description
char 1 byte -128 to 127 Stores ASCII values / characters
short 2 bytes -32,768 to 32,767 Small integers
int 4 bytes ~ ±2.14 Billion Default standard integer
long 8 bytes (4 bytes on 32-bit) ~ ±9.22 Quintillion Platform-dependent integer
long long 8 bytes ~ ±9.22 Quintillion Guaranteed to be at least 64-bit

[!NOTE] To use integers with guaranteed bit-widths across different systems (essential for embedded and hardware-level programming), include <cstdint> and use fixed-width types: - int8_t, int16_t, int32_t, int64_t (Signed) - uint8_t, uint16_t, uint32_t, uint64_t (Unsigned)

Floating-Point Types

Type Size Precision Typical Usage
float 4 bytes ~7 decimal digits Fast calculations, GPU usage
double 8 bytes ~15 decimal digits Default for floating-point literals
long double 8/16 bytes Platform-dependent High precision math

5. Modern Constants: const vs constexpr vs consteval

Modern C++ has robust mechanisms to declare constant values and evaluate expressions at compile time.

1
2
3
4
5
6
7
                  [Compile Time]                         [Runtime]
                       │                                     │
 consteval (C++20) ◄───┼─────────────────────────────────────┤
                       │                                     │
 constexpr ◄───────────┼─────────────────── (Depends) ───────┤
                       │                                     │
 const ◄───────────────┼─────────────────────────────────────┼─── (Always Checked)
  • const: Stands for "read-only." Its value can be determined at runtime, but once initialized, it cannot be modified.
  • constexpr: Tells the compiler that the value can be computed at compile time. This allows the compiler to optimize out the calculation and embed the result directly in the binary.
  • consteval (C++20): Implies that the function/expression must be evaluated at compile time. It is a compile error if it cannot be.
int get_sensor_reading() { return 42; } // Evaluated at runtime

int main() {
    const int val1 = get_sensor_reading(); // OK: determined at runtime

    // constexpr int val2 = get_sensor_reading(); 
    // ERROR: get_sensor_reading() cannot be evaluated at compile-time!

    constexpr int max_buffer = 1024 * 4; // OK: compile-time constant
}

6. Structured Bindings (C++17)

Structured bindings allow you to unpack objects, arrays, or tuples into separate variables using a clean syntax.

#include <iostream>
#include <tuple>

struct Point {
    double x;
    double y;
};

int main() {
    Point pt{3.5, 4.2};

    // Unpack struct members directly into two variables
    auto [posX, posY] = pt; 

    std::cout << "X: " << posX << ", Y: " << posY << "\n";

    // Works with std::pair / std::tuple as well
    std::pair<int, std::string> error{404, "Not Found"};
    auto [code, message] = error;

    return 0;
}

7. Type Aliases (using)

C++ allows you to create aliases for complex types. While the older C-style typedef is still valid, the modern using syntax is preferred because it is more readable and supports templates.

1
2
3
4
5
6
// C-style alias (not recommended in modern C++)
typedef unsigned char byte;

// Modern C++ alias (preferred)
using byte = unsigned char;
using GpioConfigMap = std::unordered_map<int, std::string>;

8. Type Conversions and Casting

Avoid implicit type conversions that might hide bugs. When you need to cast one type to another, use C++ explicit cast operators rather than C-style casts.

Why Avoid C-style Casts?

C-style casts (int)value are aggressive. They try a static_cast, a const_cast, and a reinterpret_cast behind the scenes, which can lead to silent bugs.

Explicit C++ Casts

  1. static_cast: The most common cast. Used for safe compile-time conversions (e.g. double to int, float to double).
    double pi = 3.14159;
    int approximation = static_cast<int>(pi); // Value is 3
    
  2. const_cast: Used to strip away the const qualifier of a variable (use with caution).
  3. reinterpret_cast: Casts any pointer type to any other pointer type. Used for low-level memory operations (e.g. mapping physical register addresses).