⚙️ Unit 08 • C++ Systems Engineering

Structures, Unions & Enumeration Architecture

User-defined data aggregation and type systems in C++: structure definitions, memory alignment and padding rules, arrays and nesting, pointer member access via the arrow operator (->), pass-by-const-reference optimization, memory-sharing unions, type-safe scoped enumerations (enum class), and modern type aliasing (typedef vs using).

📐 Alignment: Natural Word Boundary Padding
📦 Unions: Shared Base Address & Largest-Member Size
🏷️ Enums: Scoped (enum class) Strongly-Typed Values
🔤 Aliasing: typedef & Modern C++11 using
01

Structures: User-Defined Composite Types & Memory Alignment

A structure (struct) in C++ is a user-defined composite data type that aggregates logically related variables of differing (heterogeneous) data types into a single named memory unit. In C++, unlike legacy C, defining a structure automatically establishes a first-class type name without requiring repeated struct keyword prefixes.

Structure Definition & Initialization

Structures encapsulate contiguous data members. In modern C++ (C++11/20), structures support uniform brace initialization, designated initializers, and default member values:

struct ServerNode {
    int nodeId;         // 4 bytes
    double cpuLoad;     // 8 bytes
    bool isOnline;      // 1 byte
};

// Uniform Brace Initialization
ServerNode s1{101, 45.2, true};
Hardware Memory Alignment & Padding

Modern 64-bit CPUs read memory words across 4-byte or 8-byte boundaries. Placing an 8-byte double at an odd memory address causes performance penalties or hardware traps. Therefore, compilers insert invisible padding bytes to guarantee natural alignment.

Structure Memory Layout with Alignment Padding (Total: 24 Bytes):
struct ServerNode { char tag; /* 1B */ int id; /* 4B */ double load; /* 8B */ };
char tag
(1 Byte)
Padding Bytes
(3 Bytes pad)
int id
(4 Bytes)
double load
(8 Bytes)
Without padding, total size would be $1 + 4 + 8 = 13$ bytes. Due to natural alignment to an 8-byte boundary, the compiler pads to 16 bytes (or 24 bytes depending on trailing padding).
⚠️ Ordering Struct Members to Minimize Memory Bloat

By ordering structure members from largest data type to smallest data type (e.g., placing double first, followed by int, then char), you minimize or eliminate internal padding bytes, significantly reducing the memory footprint across large collections of objects!

02

Array of Structures & Nested Structures

Structures can be organized into contiguous collections (arrays) and composed hierarchically (nesting) to mirror complex, multi-tiered systems data:

Array of Structures

Allocates contiguous memory blocks, where each element is an entire struct instance. Ideal for tables, particle systems, and hardware sensor telemetry:

ServerNode cluster[3] = {
    {1, 12.4, true},
    {2, 98.1, false},
    {3, 44.0, true}
};
// Accessing element 1's cpuLoad:
cluster[1].cpuLoad = 85.0;
Nested Structures

Embedding one structure type as a member inside another allows natural hierarchical modeling without redundant variable definitions:

struct NetworkConfig {
    char ip[16];
    int port;
};

struct EdgeDevice {
    int deviceId;
    NetworkConfig net; // Nested
};
// device.net.port = 8080;
nested_and_arrays.cpp
#include <iostream>

struct Coordinates {
    double latitude;
    double longitude;
};

struct DroneSensor {
    int sensorId;
    Coordinates position; // Nested structure
    float batteryLevel;
};

int main() {
    // Array of nested structures
    DroneSensor fleet[2] = {
        {101, {37.7749, -122.4194}, 94.5f},
        {102, {40.7128, -74.0060}, 62.0f}
    };

    for (int i = 0; i < 2; ++i) {
        std::cout << "Drone #" << fleet[i].sensorId 
                  << " Lat: " << fleet[i].position.latitude
                  << " Battery: " << fleet[i].batteryLevel << "%\n";
    }

    return 0;
}
03

Pointers to Structures & The Arrow Operator (->)

When passing structures into functions, engineers must distinguish between Pass-by-Value (which duplicates the entire memory payload onto the call stack) and Pass-by-Const-Reference / Pass-by-Pointer (which passes a zero-overhead 8-byte address):

Passing Technique Syntax Pattern Stack Cost Mutation Risk Recommendation
Pass by Value void print(Node n) sizeof(Node) bytes copied None (caller original isolated) Anti-pattern for large structures
Pass by Pointer void update(Node* n) 8 bytes (pointer size) High (mutates original via *n) Required when reassigning address
Pass by Const Ref void render(const Node& n) 8 bytes (underlying address) Zero (enforced read-only) Standard C++ Best Practice
The Arrow Member Access Operator (->)

ptr->member ≡ (*ptr).member

Because the dot operator (.) has higher operator precedence than the dereference operator (*), writing *ptr.member erroneously evaluates to *(ptr.member). The arrow operator (->) provides clean syntax for dereferencing a pointer and accessing a member in a single operation.

structure_pointers.cpp
#include <iostream>

struct Telemetry {
    int packetId;
    float temperature;
    float pressure;
};

// 1. Mutating original structure via pointer
void calibrate(Telemetry* t) {
    if (t != nullptr) {
        t->temperature -= 1.5f; // Equivalent to (*t).temperature -= 1.5f;
    }
}

// 2. High-performance read-only inspection (Zero-copy)
void displayTelemetry(const Telemetry& t) {
    std::cout << "Packet: " << t.packetId 
              << " | Temp: " << t.temperature 
              << " | Pres: " << t.pressure << "\n";
}

int main() {
    Telemetry t1{5001, 24.5f, 1013.2f};
    
    Telemetry* tPtr = &t1;
    calibrate(tPtr);
    displayTelemetry(t1);

    return 0;
}
04

Unions: Shared Memory Space Architecture

A union is a user-defined data type where all declared members share the exact same starting memory address. While a structure's size is the sum of its member sizes plus padding, a union's size is equal only to the size of its largest member:

Feature Structure (struct) Union (union)
Memory Allocation Each member receives its own distinct memory offset All members share the exact same starting address
Total Size Sum of all member sizes + alignment padding Equal to the size of the single largest member (padded to alignment)
Member Concurrency All members can store active values simultaneously Only one member can hold an active value at any instant
Primary Purpose Grouping heterogeneous data into an entity Memory conservation, protocol packet decoding, register bitfields
Union Memory Model (Total Allocated: 8 Bytes):
union Register { int asInt; /* 4B */ double asDouble; /* 8B */ char asByte; /* 1B */ };
Shared Base Address: 0x8000
All 3 members (asInt, asDouble, asByte) begin at 0x8000.
Writing asInt = 42 mutates the exact byte cells occupied by asDouble!
union_demo.cpp
#include <iostream>

union DataPacket {
    int intVal;
    double doubleVal;
    char charVal;
};

int main() {
    DataPacket packet;
    std::cout << "Size of union: " << sizeof(packet) << " bytes\n"; // 8 bytes (size of double)

    packet.intVal = 65;
    std::cout << "intVal: " << packet.intVal << "\n"; // 65

    // Writing to doubleVal overwrites the memory occupied by intVal!
    packet.doubleVal = 3.14159;
    std::cout << "doubleVal: " << packet.doubleVal << "\n";
    
    // Reading intVal now yields undefined garbage bit interpretation:
    std::cout << "Garbage intVal after double write: " << packet.intVal << "\n";

    return 0;
}
05

Enumerations (enum vs enum class) & typedef Aliasing

An enumeration defines a custom type whose values are restricted to a developer-specified set of named integral constants:

Legacy C-Style Unscoped enum
  • Pollutes the enclosing scope with identifier names.
  • Implicitly converts to integers, enabling illogical arithmetic (e.g., Color::RED == Status::ERROR).
  • Prone to name collisions across multiple enums.
Modern C++11 Scoped enum class
  • Strictly scoped: must be accessed via EnumName::Enumerator.
  • Strongly typed: no implicit conversion to int.
  • Underlying integral type can be explicitly specified (e.g., : uint8_t to save RAM).
enums_and_aliases.cpp
#include <iostream>
#include <cstdint>

// 1. Modern C++11 Scoped Enum with explicit 1-byte storage:
enum class HttpStatus : uint16_t {
    OK = 200,
    Created = 201,
    BadRequest = 400,
    NotFound = 404,
    InternalError = 500
};

// 2. Legacy 'typedef' vs Modern C++11 'using' alias:
typedef unsigned long long u64_t;      // Legacy C style
using Microseconds = uint64_t;          // Modern C++ preferred style

void checkStatus(HttpStatus status) {
    if (status == HttpStatus::OK) {
        std::cout << "Request processed successfully.\n";
    } else if (status == HttpStatus::NotFound) {
        std::cout << "Resource not found (404).\n";
    }
}

int main() {
    HttpStatus code = HttpStatus::OK;
    checkStatus(code);

    // Explicit cast required to extract numerical integer value:
    std::cout << "Integer code: " << static_cast<uint16_t>(code) << "\n";

    Microseconds duration = 250000;
    std::cout << "Timer latency: " << duration << " us\n";

    return 0;
}
1
Legacy C Era: typedef struct & Unscoped enum
Required typedef struct Node { ... } Node; to avoid typing struct Node. Enums leaked symbols into global scope.
2
Classic C++ Era: First-Class Struct Names
C++ elevated struct to a first-class type without typedef. Structs became identical to classes except default public access.
3
Modern C++11/20 Era: enum class & Type Aliases (using)
Type-safe scoped enums, template-compatible using NewType = OldType;, and designated initializers for zero-overhead safety.
06

Viva Voce & Engineering Interview Questions

Q1 • Architecture What is structure padding, and what hardware constraint causes compilers to insert it?
Compilers insert structure padding (unused alignment bytes) between members to align data types to addresses that are natural multiples of their sizes (e.g., 4-byte integers on 4-byte boundaries, 8-byte doubles on 8-byte boundaries). Modern CPU memory controllers access bus lines in 32-bit or 64-bit words; misaligned memory accesses require two separate memory fetch cycles and bitwise shifting operations, creating a significant performance penalty.
Q2 • Language Design In C++, what is the exact difference between a struct and a class?
In C++, a struct and a class are functionally identical with only two syntactic default differences: (1) Members and base classes of a struct default to public access, whereas in a class they default to private access; (2) Struct inheritance defaults to public, while class inheritance defaults to private. Both can contain constructors, methods, destructors, and virtual tables.
Q3 • Member Access Why is ptr->member preferred over (*ptr).member?
Because the dot (.) operator has higher precedence than the unary dereference (*) operator, writing *ptr.member compiles as *(ptr.member), attempting to access a member on a raw address before dereferencing, which fails compilation. (*ptr).member requires clumsy parentheses, whereas the arrow operator (->) provides unambiguous, readable syntax for pointer dereferencing and member lookup.
Q4 • Memory Layout How is the size of a union calculated, and what is its alignment requirement?
The memory footprint of a union is equal to the size of its largest declared data member, rounded up to the nearest multiple of the largest alignment requirement among its members. All declared members start at offset 0 (identical starting base address).
Q5 • Type Safety Why should modern C++ software avoid unscoped enum in favor of enum class?
Legacy unscoped enum pollutes the surrounding namespace (enumerator names cannot collide with any other identifier in that scope) and implicitly converts to raw integers, causing silent logical bugs when comparing enums of entirely different semantic domains. enum class provides explicit namespacing (Color::Red) and forbids implicit integer conversion without an explicit cast.
Q6 • Modern Standards What advantage does using NewName = OldType; have over legacy typedef OldType NewName;?
The modern using syntax follows the intuitive assignment pattern (left-hand side name assigned to right-hand side type). Crucially, using can be templated to create alias templates (e.g., template<typename T> using StringMap = std::map<std::string, T>;), which is syntactically impossible with legacy typedef.
Q7 • Performance Why should non-trivial structures always be passed to functions by const T&?
Passing by value requires pushing a complete bitwise or copy-constructed replica of all struct members onto the function's stack frame. For large structs, this consumes excessive stack memory and burns CPU cycles copying data. Passing by const T& passes only an 8-byte memory address while providing compile-time read-only guarantees.
Q8 • Systems Low-Level What does the #pragma pack(1) directive do, and what are its performance trade-offs?
#pragma pack(1) instructs the compiler to disable all automatic memory padding and pack struct members with 1-byte alignment. This eliminates memory waste (ideal for serializing network packets or binary file headers matching exact hardware wire formats), but causes significant runtime performance penalties on architectures that require multi-cycle memory fetches for unaligned addresses.