C++ for Hardware¶
C++ is essential for ATE (Automatic Test Equipment) programming, firmware development, and performance-critical validation applications. This guide covers C++ fundamentals with a focus on hardware validation and embedded systems.
Why C++ for Validation?¶
| Advantage | Description |
|---|---|
| Performance | Direct hardware access, minimal overhead |
| Memory Control | Manual memory management for predictable behavior |
| ATE Support | Native language for Teradyne, Advantest platforms |
| Firmware | Embedded systems, bare-metal programming |
| Legacy Code | Large existing codebase in validation |
Embedded C++ Essentials¶
The volatile Keyword¶
The volatile keyword tells the compiler that a variable's value may change at any time, preventing optimization that could skip reads/writes.
// Hardware register - MUST be volatile
volatile uint32_t* const STATUS_REG = (uint32_t*)0x40001000;
volatile uint32_t* const CONTROL_REG = (uint32_t*)0x40001004;
volatile uint32_t* const DATA_REG = (uint32_t*)0x40001008;
// Without volatile - WRONG! Compiler may optimize away reads
uint32_t* status = (uint32_t*)0x40001000;
while (*status == 0) { } // May become infinite loop!
// With volatile - CORRECT
volatile uint32_t* status_v = (volatile uint32_t*)0x40001000;
while (*status_v == 0) { } // Always reads from hardware
// Example: Waiting for hardware ready bit
void wait_for_ready() {
volatile uint32_t* status = (volatile uint32_t*)0x40001000;
// Poll until bit 0 (READY) is set
while ((*status & 0x01) == 0) {
// Compiler MUST read status each iteration
}
}
// Example: Interrupt flag cleared by hardware
volatile bool interrupt_occurred = false;
void ISR_Handler() {
interrupt_occurred = true; // Set in ISR
}
void main_loop() {
while (!interrupt_occurred) {
// Without volatile, compiler might cache the value
}
interrupt_occurred = false;
}
// Memory-mapped peripheral structure
struct __attribute__((packed)) UART_Regs {
volatile uint32_t DATA; // 0x00 - Data register
volatile uint32_t STATUS; // 0x04 - Status register
volatile uint32_t CONTROL; // 0x08 - Control register
volatile uint32_t BAUD; // 0x0C - Baud rate register
};
volatile UART_Regs* const UART0 = (UART_Regs*)0x40010000;
void uart_send(uint8_t byte) {
// Wait for TX buffer empty
while (!(UART0->STATUS & (1 << 7))) { }
UART0->DATA = byte;
}
When to Use volatile
- Memory-mapped hardware registers
- Variables modified by ISRs (Interrupt Service Routines)
- Variables shared between threads (consider
std::atomicinstead) - DMA buffers
Never use volatile for thread synchronization - use std::atomic or mutexes.
The const Keyword¶
const provides compile-time guarantees about immutability and enables compiler optimizations.
// 1. Const variables - value cannot change
const uint32_t MAX_BUFFER_SIZE = 1024;
const double PI = 3.14159265359;
// 2. Const pointers vs pointer to const
int value = 10;
int another = 20;
const int* ptr1 = &value; // Pointer to const int (data is const)
// *ptr1 = 20; // ERROR: Cannot modify data
ptr1 = &another; // OK: Can change pointer
int* const ptr2 = &value; // Const pointer to int (pointer is const)
*ptr2 = 20; // OK: Can modify data
// ptr2 = &another; // ERROR: Cannot change pointer
const int* const ptr3 = &value; // Both const
// *ptr3 = 20; // ERROR
// ptr3 = &another; // ERROR
// 3. Const member functions - don't modify object
class Device {
private:
uint32_t base_address_;
bool initialized_;
public:
// Const method - promises not to modify member variables
uint32_t getBaseAddress() const {
return base_address_;
}
bool isInitialized() const {
return initialized_;
}
// Non-const method - may modify object
void initialize() {
initialized_ = true;
}
// Const-correct read function
uint32_t readRegister(uint32_t offset) const {
volatile uint32_t* reg = (volatile uint32_t*)(base_address_ + offset);
return *reg;
}
};
// 4. Const references - efficient parameter passing
void processData(const std::vector<uint32_t>& data) {
// Cannot modify data, but no copy made
for (const auto& value : data) {
// Process value
}
}
// 5. Const return values
class Config {
private:
std::string name_;
public:
// Return const reference to prevent modification
const std::string& getName() const {
return name_;
}
};
// 6. constexpr - Compile-time constants (C++11+)
constexpr uint32_t CLOCK_FREQ = 100000000; // 100 MHz
constexpr uint32_t calculateBaudDivisor(uint32_t baud) {
return CLOCK_FREQ / (16 * baud);
}
constexpr uint32_t BAUD_115200_DIV = calculateBaudDivisor(115200); // Computed at compile time
Bitwise Operators Deep Dive¶
#include <cstdint>
#include <bitset>
// ============================================================
// BITWISE OPERATORS
// ============================================================
// AND (&) - Mask/clear bits
uint32_t reg = 0xABCD1234;
uint32_t masked = reg & 0x0000FFFF; // Keep lower 16 bits: 0x1234
uint32_t bit_check = reg & (1 << 5); // Check if bit 5 is set
// OR (|) - Set bits
uint32_t with_bit_set = reg | (1 << 7); // Set bit 7
uint32_t combined = 0x00 | 0x01 | 0x04 | 0x10; // Set bits 0, 2, 4
// XOR (^) - Toggle bits
uint32_t toggled = reg ^ 0x0000FFFF; // Toggle lower 16 bits
uint32_t swapped = a ^ b; b = a ^ b; a = swapped; // XOR swap trick
// NOT (~) - Invert all bits
uint32_t inverted = ~reg; // All bits flipped
uint32_t clear_mask = ~(1 << 5); // All 1s except bit 5
// Left Shift (<<) - Multiply by 2^n
uint32_t mul_8 = value << 3; // Multiply by 8
uint32_t bit_position = 1 << n; // Set bit at position n
// Right Shift (>>) - Divide by 2^n
uint32_t div_4 = value >> 2; // Divide by 4
uint32_t upper_byte = reg >> 24; // Get bits [31:24]
// ============================================================
// COMMON BIT MANIPULATION PATTERNS
// ============================================================
// Bit manipulation macros (widely used in embedded)
#define BIT(n) (1UL << (n))
#define SET_BIT(reg, n) ((reg) |= BIT(n))
#define CLR_BIT(reg, n) ((reg) &= ~BIT(n))
#define TGL_BIT(reg, n) ((reg) ^= BIT(n))
#define GET_BIT(reg, n) (((reg) >> (n)) & 1UL)
#define CHK_BIT(reg, n) ((reg) & BIT(n))
// Multi-bit field macros
#define FIELD_MASK(width) ((1UL << (width)) - 1)
#define GET_FIELD(reg, pos, width) (((reg) >> (pos)) & FIELD_MASK(width))
#define SET_FIELD(reg, pos, width, val) \
((reg) = ((reg) & ~(FIELD_MASK(width) << (pos))) | (((val) & FIELD_MASK(width)) << (pos)))
// Example usage
uint32_t status = 0x12345678;
uint32_t field = GET_FIELD(status, 8, 4); // Extract 4 bits starting at bit 8
SET_FIELD(status, 16, 8, 0xAB); // Set 8-bit field at bit 16
// ============================================================
// BIT FIELD EXTRACTION FUNCTIONS
// ============================================================
// Extract bit field [msb:lsb]
inline uint32_t extract_bits(uint32_t value, int msb, int lsb) {
uint32_t width = msb - lsb + 1;
uint32_t mask = (1UL << width) - 1;
return (value >> lsb) & mask;
}
// Insert value into bit field [msb:lsb]
inline uint32_t insert_bits(uint32_t reg, uint32_t value, int msb, int lsb) {
uint32_t width = msb - lsb + 1;
uint32_t mask = (1UL << width) - 1;
reg &= ~(mask << lsb); // Clear field
reg |= (value & mask) << lsb; // Insert value
return reg;
}
// Example: PCIe TLP Header parsing
uint32_t tlp_header = 0x40000001;
uint32_t fmt = extract_bits(tlp_header, 30, 29); // Format field
uint32_t type = extract_bits(tlp_header, 28, 24); // Type field
uint32_t length = extract_bits(tlp_header, 9, 0); // Length field
// ============================================================
// USEFUL BIT TRICKS
// ============================================================
// Check if power of 2
bool is_power_of_2(uint32_t n) {
return n && !(n & (n - 1));
}
// Count set bits (population count)
int popcount(uint32_t n) {
int count = 0;
while (n) {
count += n & 1;
n >>= 1;
}
return count;
}
// Or use built-in: __builtin_popcount(n)
// Find first set bit (from LSB)
int find_first_set(uint32_t n) {
if (n == 0) return -1;
return __builtin_ffs(n) - 1; // GCC built-in
}
// Round up to next power of 2
uint32_t next_power_of_2(uint32_t n) {
n--;
n |= n >> 1;
n |= n >> 2;
n |= n >> 4;
n |= n >> 8;
n |= n >> 16;
return n + 1;
}
// Byte swap (endian conversion)
uint32_t byte_swap_32(uint32_t val) {
return ((val >> 24) & 0x000000FF) |
((val >> 8) & 0x0000FF00) |
((val << 8) & 0x00FF0000) |
((val << 24) & 0xFF000000);
}
// Or use: __builtin_bswap32(val)
// Align to boundary
inline uint32_t align_up(uint32_t value, uint32_t alignment) {
return (value + alignment - 1) & ~(alignment - 1);
}
inline uint32_t align_down(uint32_t value, uint32_t alignment) {
return value & ~(alignment - 1);
}
Bit Fields in Structures¶
#include <cstdint>
// Bit fields allow compact storage and direct bit access
// WARNING: Bit field layout is implementation-defined!
// PCIe Configuration Space Header (Type 0)
struct __attribute__((packed)) PCIeConfigHeader {
uint16_t vendor_id;
uint16_t device_id;
uint16_t command;
uint16_t status;
uint8_t revision_id;
uint8_t prog_if;
uint8_t subclass;
uint8_t class_code;
uint8_t cache_line_size;
uint8_t latency_timer;
uint8_t header_type;
uint8_t bist;
uint32_t bar[6];
uint32_t cardbus_cis;
uint16_t subsystem_vendor_id;
uint16_t subsystem_id;
uint32_t expansion_rom;
uint8_t capabilities_ptr;
uint8_t reserved[7];
uint8_t interrupt_line;
uint8_t interrupt_pin;
uint8_t min_grant;
uint8_t max_latency;
};
// Using bit fields for hardware registers
struct StatusRegister {
uint32_t ready : 1; // Bit 0
uint32_t busy : 1; // Bit 1
uint32_t error : 1; // Bit 2
uint32_t reserved1 : 5; // Bits 3-7
uint32_t error_code : 8; // Bits 8-15
uint32_t count : 12; // Bits 16-27
uint32_t reserved2 : 4; // Bits 28-31
};
// Union for dual access (raw and structured)
union HardwareRegister {
uint32_t raw;
struct {
uint32_t enable : 1;
uint32_t mode : 3;
uint32_t speed : 4;
uint32_t reserved : 8;
uint32_t data : 16;
} fields;
};
// Usage
HardwareRegister reg;
reg.raw = 0x12340000; // Write raw value
reg.fields.enable = 1; // Set enable bit
reg.fields.mode = 5; // Set mode field
uint32_t value = reg.raw; // Read as 32-bit
// Type-safe register access class
template<typename T>
class Register {
private:
volatile T* addr_;
public:
explicit Register(uintptr_t address)
: addr_(reinterpret_cast<volatile T*>(address)) {}
T read() const { return *addr_; }
void write(T value) { *addr_ = value; }
void set_bits(T mask) { *addr_ |= mask; }
void clear_bits(T mask) { *addr_ &= ~mask; }
void toggle_bits(T mask) { *addr_ ^= mask; }
bool test_bit(int bit) const { return (*addr_ >> bit) & 1; }
};
Memory Alignment and Packing¶
#include <cstdint>
#include <cstddef>
// Natural alignment (compiler default)
struct NaturalAlignment {
char a; // 1 byte, offset 0
// 3 bytes padding
int b; // 4 bytes, offset 4
char c; // 1 byte, offset 8
// 3 bytes padding
}; // Total: 12 bytes (with padding)
// Packed structure (no padding) - for hardware registers/protocols
struct __attribute__((packed)) PackedStruct {
char a; // 1 byte, offset 0
int b; // 4 bytes, offset 1
char c; // 1 byte, offset 5
}; // Total: 6 bytes (no padding)
// Explicit alignment
struct alignas(16) AlignedStruct {
uint32_t data[4];
}; // Aligned to 16-byte boundary
// Check alignment
static_assert(alignof(AlignedStruct) == 16, "Alignment check");
static_assert(sizeof(PackedStruct) == 6, "Size check");
// DMA buffer with alignment requirement
struct alignas(64) DMABuffer {
uint8_t data[4096]; // Cache-line aligned for DMA
};
// Memory-mapped register block (must match hardware layout)
struct __attribute__((packed)) GPIORegisters {
volatile uint32_t DATA; // 0x00
volatile uint32_t DIR; // 0x04
volatile uint32_t INT_ENABLE; // 0x08
volatile uint32_t INT_STATUS; // 0x0C
volatile uint32_t INT_CLEAR; // 0x10
uint32_t reserved[3]; // 0x14 - 0x1F
volatile uint32_t ALT_FUNC; // 0x20
};
static_assert(offsetof(GPIORegisters, ALT_FUNC) == 0x20, "Offset check");
// Pointer alignment check
bool is_aligned(void* ptr, size_t alignment) {
return (reinterpret_cast<uintptr_t>(ptr) % alignment) == 0;
}
Function Pointers and Callbacks¶
#include <functional>
#include <cstdint>
// ============================================================
// FUNCTION POINTERS (C-style)
// ============================================================
// Function pointer typedef
typedef void (*ISR_Handler)(void);
typedef uint32_t (*ReadFunc)(uint32_t address);
typedef void (*WriteFunc)(uint32_t address, uint32_t value);
// Function pointer declaration
void (*callback)(int status);
// Interrupt vector table
ISR_Handler vector_table[256] = {nullptr};
void register_isr(int irq_num, ISR_Handler handler) {
vector_table[irq_num] = handler;
}
void timer_isr() {
// Handle timer interrupt
}
// Register the ISR
register_isr(15, timer_isr);
// Function pointer for hardware abstraction
struct HardwareInterface {
ReadFunc read;
WriteFunc write;
void (*init)(void);
void (*deinit)(void);
};
uint32_t pcie_read(uint32_t addr) { /* ... */ return 0; }
void pcie_write(uint32_t addr, uint32_t val) { /* ... */ }
void pcie_init() { /* ... */ }
HardwareInterface pcie_interface = {
.read = pcie_read,
.write = pcie_write,
.init = pcie_init,
.deinit = nullptr
};
// ============================================================
// MODERN C++ CALLBACKS
// ============================================================
// std::function - more flexible than function pointers
#include <functional>
class Device {
public:
using Callback = std::function<void(int status, uint32_t data)>;
private:
Callback completion_callback_;
public:
void setCallback(Callback cb) {
completion_callback_ = std::move(cb);
}
void onOperationComplete(int status, uint32_t data) {
if (completion_callback_) {
completion_callback_(status, data);
}
}
};
// Usage with lambda
Device dev;
dev.setCallback([](int status, uint32_t data) {
if (status == 0) {
printf("Success: data = 0x%08X\n", data);
}
});
// Usage with member function
class Controller {
public:
void handleCompletion(int status, uint32_t data) {
// Handle completion
}
};
Controller ctrl;
dev.setCallback([&ctrl](int s, uint32_t d) {
ctrl.handleCompletion(s, d);
});
Static Keyword Usage¶
#include <cstdint>
// ============================================================
// 1. STATIC LOCAL VARIABLE - Persists across function calls
// ============================================================
uint32_t getNextSequenceNumber() {
static uint32_t sequence = 0; // Initialized once, persists
return sequence++;
}
// Singleton pattern using static local
class Logger {
public:
static Logger& getInstance() {
static Logger instance; // Thread-safe in C++11+
return instance;
}
void log(const char* msg) { /* ... */ }
private:
Logger() = default; // Private constructor
};
// ============================================================
// 2. STATIC MEMBER VARIABLE - Shared across all instances
// ============================================================
class Device {
private:
static int device_count_; // Shared by all instances
int device_id_;
public:
Device() : device_id_(device_count_++) {}
static int getDeviceCount() { return device_count_; }
int getDeviceId() const { return device_id_; }
};
// Must define static member in .cpp file
int Device::device_count_ = 0;
// ============================================================
// 3. STATIC MEMBER FUNCTION - No 'this' pointer, can't access non-static members
// ============================================================
class MathUtils {
public:
static uint32_t crc32(const uint8_t* data, size_t len) {
// Can be called without object: MathUtils::crc32(...)
return 0;
}
static constexpr double PI = 3.14159265359;
};
// ============================================================
// 4. STATIC GLOBAL/FILE SCOPE - Internal linkage (not visible outside file)
// ============================================================
// In .cpp file:
static uint32_t internal_buffer[256]; // Only visible in this file
static void helper_function() { } // Only visible in this file
// Preferred C++ way: anonymous namespace
namespace {
uint32_t private_data[256];
void private_helper() { }
}
// ============================================================
// 5. STATIC IN CLASS TEMPLATE
// ============================================================
template<typename T>
class Container {
static size_t instance_count; // One per type instantiation
};
template<typename T>
size_t Container<T>::instance_count = 0;
// Container<int>::instance_count is separate from Container<float>::instance_count
Inline Functions¶
#include <cstdint>
// ============================================================
// INLINE FUNCTIONS - Request compiler to insert code at call site
// ============================================================
// Inline function (hint to compiler)
inline uint32_t read_reg(volatile uint32_t* addr) {
return *addr;
}
inline void write_reg(volatile uint32_t* addr, uint32_t value) {
*addr = value;
}
// Inline in header file (must be inline if defined in header)
// utils.h
inline int max(int a, int b) {
return (a > b) ? a : b;
}
// Force inline (compiler-specific)
#ifdef __GNUC__
#define FORCE_INLINE __attribute__((always_inline)) inline
#elif defined(_MSC_VER)
#define FORCE_INLINE __forceinline
#else
#define FORCE_INLINE inline
#endif
FORCE_INLINE uint32_t fast_read(volatile uint32_t* addr) {
return *addr;
}
// Inline class methods (defined inside class are implicitly inline)
class FastMath {
public:
// Implicitly inline
int square(int x) { return x * x; }
// Explicitly inline for out-of-class definition
inline int cube(int x);
};
inline int FastMath::cube(int x) {
return x * x * x;
}
// constexpr functions are implicitly inline
constexpr uint32_t compile_time_calc(uint32_t x) {
return x * x + 1;
}
// ============================================================
// WHEN TO USE INLINE
// ============================================================
// Good candidates:
// - Small functions (1-3 lines)
// - Frequently called in tight loops
// - Accessor methods (getters/setters)
// - Performance-critical register access
// Bad candidates:
// - Large functions (code bloat)
// - Functions with loops
// - Recursive functions
// - Virtual functions (can't be inlined through pointer)
Data Types¶
Fixed-Width Integer Types¶
#include <cstdint>
#include <string>
// Fixed-width integer types (preferred for hardware)
int8_t signed_byte; // -128 to 127
uint8_t unsigned_byte; // 0 to 255
int16_t signed_word; // -32768 to 32767
uint16_t unsigned_word; // 0 to 65535
int32_t signed_dword; // -2^31 to 2^31-1
uint32_t unsigned_dword; // 0 to 2^32-1
int64_t signed_qword; // -2^63 to 2^63-1
uint64_t unsigned_qword; // 0 to 2^64-1
// Pointer-sized integers
uintptr_t address; // Can hold any pointer value
ptrdiff_t difference; // Pointer difference
// Floating point
float single_precision; // 32-bit
double double_precision; // 64-bit
// Boolean
bool test_passed = true;
// Character and strings
char single_char = 'A';
std::string device_name = "DDR4_DIMM";
// Arrays
uint32_t registers[16];
double voltages[4] = {1.2, 1.2, 1.2, 1.2};
// Pointers
uint32_t* reg_ptr = ®isters[0];
uint32_t value = *reg_ptr; // Dereference
Memory Management¶
Stack vs Heap¶
#include <memory>
#include <vector>
void memory_examples() {
// Stack allocation (automatic, fast)
int stack_array[100]; // Fixed size on stack
double voltage = 1.2; // Simple types on stack
// Heap allocation (dynamic, manual)
int* heap_array = new int[1000];
// ... use array ...
delete[] heap_array; // Must delete!
// Smart pointers (modern C++, automatic cleanup)
auto unique_ptr = std::make_unique<int[]>(1000);
auto shared_ptr = std::make_shared<DeviceInfo>();
// Containers (automatic memory management)
std::vector<double> measurements;
measurements.push_back(1.2);
measurements.push_back(1.3);
} // All automatic cleanup at end of scope
RAII Pattern¶
#include <fstream>
// Resource Acquisition Is Initialization
class FileHandler {
private:
std::ofstream file;
public:
FileHandler(const std::string& filename)
: file(filename) {
if (!file.is_open()) {
throw std::runtime_error("Failed to open file");
}
}
~FileHandler() {
if (file.is_open()) {
file.close();
}
}
void write(const std::string& data) {
file << data;
}
// Prevent copying
FileHandler(const FileHandler&) = delete;
FileHandler& operator=(const FileHandler&) = delete;
};
// Usage - file automatically closed when handler goes out of scope
void log_results() {
FileHandler log("results.txt");
log.write("Test passed\n");
} // File automatically closed here
Classes and Inheritance¶
Class Definition¶
#include <string>
#include <cstdint>
#include <stdexcept>
class MemoryDevice {
private:
std::string name_;
uint32_t base_address_;
bool initialized_;
protected:
uint64_t density_;
public:
// Constructor
MemoryDevice(const std::string& name, uint32_t base_addr)
: name_(name)
, base_address_(base_addr)
, initialized_(false)
, density_(0) {}
// Virtual destructor for inheritance
virtual ~MemoryDevice() = default;
// Getter methods
const std::string& name() const { return name_; }
uint32_t base_address() const { return base_address_; }
bool is_initialized() const { return initialized_; }
// Virtual methods (can be overridden)
virtual bool initialize() {
initialized_ = true;
return true;
}
// Pure virtual method (must be implemented by derived classes)
virtual void reset() = 0;
};
// Derived class
class DDR4Device : public MemoryDevice {
private:
int cas_latency_;
public:
DDR4Device(const std::string& name, uint32_t base_addr, int cl)
: MemoryDevice(name, base_addr), cas_latency_(cl) {
density_ = 8ULL * 1024 * 1024 * 1024; // 8Gb
}
void reset() override {
// DDR4-specific reset
}
};
Error Handling¶
Exception Handling¶
#include <stdexcept>
#include <string>
// Custom exception classes
class DeviceError : public std::runtime_error {
public:
explicit DeviceError(const std::string& msg)
: std::runtime_error(msg) {}
};
class TimeoutError : public DeviceError {
private:
int timeout_ms_;
public:
TimeoutError(const std::string& msg, int timeout)
: DeviceError(msg), timeout_ms_(timeout) {}
int timeout() const { return timeout_ms_; }
};
// Exception handling
void run_test() {
try {
if (!device.initialize()) {
throw DeviceError("Initialization failed");
}
}
catch (const TimeoutError& e) {
std::cerr << "Timeout: " << e.what() << std::endl;
}
catch (const DeviceError& e) {
std::cerr << "Device error: " << e.what() << std::endl;
}
catch (...) {
std::cerr << "Unknown error" << std::endl;
}
}
Error Codes Pattern (Embedded)¶
// Error codes (common in embedded/ATE)
enum class ErrorCode : int32_t {
SUCCESS = 0,
ERR_INVALID_PARAM = -1,
ERR_TIMEOUT = -2,
ERR_DEVICE_NOT_FOUND = -3,
ERR_COMMUNICATION = -4
};
ErrorCode read_device(uint32_t address, uint32_t* value) {
if (value == nullptr) {
return ErrorCode::ERR_INVALID_PARAM;
}
*value = 0x12345678;
return ErrorCode::SUCCESS;
}
Multithreading¶
Basic Threading¶
#include <thread>
#include <mutex>
#include <atomic>
// Shared data protection
std::mutex data_mutex;
std::vector<double> shared_measurements;
void measure_channel(int channel_id) {
double measurement = read_voltage(channel_id);
std::lock_guard<std::mutex> lock(data_mutex);
shared_measurements.push_back(measurement);
}
// Atomic variables for simple synchronization
std::atomic<bool> stop_flag{false};
std::atomic<int> test_count{0};
void run_parallel_tests() {
std::vector<std::thread> threads;
for (int i = 0; i < 4; i++) {
threads.emplace_back(measure_channel, i);
}
for (auto& t : threads) {
t.join();
}
}
Related Topics¶
- Data Structures & Algorithms - Interview preparation
- Design Patterns - Singleton, Factory, Observer
- Python Programming - For scripting and automation
References¶
- ISO C++ Standard
- cppreference.com
- Embedded C++ Best Practices
- ATE vendor documentation