Project 2.09 Section 2 ⚡ Embedded Relevance: Critical constexpr const #define .rodata Type Safety

2.09 Macros (#define) vs const vs constexpr: Flash ROM (.rodata) & Scope Safety

Executive Summary: Analyzing constants in C++. We contrast legacy C preprocessor macros (#define) with type-safe const and compile-time constexpr, explaining why macros lack scope and type verification, and how static constexpr constants are placed 100% in Flash ROM (.rodata) with 0 SRAM consumption.

💻 1. Annotated Source Code

#include <iostream>
#include <string>

using namespace std;

int main() {

	const double MY_PI = 3.14159;
	const string MY_NAME = "John";

	cout << MY_PI << endl;

	//MY_NAME = "Rob";

	cout << MY_NAME << endl;

	return 0;
}

📐 2. Architecture & UML Class Model

📐 Compile-Time Constants & Flash ROM (.rodata) Model
+ Public - Private # Protected
<<struct>> ConstexprMemoryPool .rodata Placement (Zero SRAM)
+PI : constexpr double = 3.141592653589793
+MAX_BUFFER_SIZE : constexpr size_t = 128
+DEVICE_ID : constexpr uint32_t = 0xAA5500FF
+calculateCircumference(radius: double) : constexpr double

📚 3. Core C++ Concepts Deep-Dive

1. Legacy #define Macros

#define BUFFER_SIZE 64 is a dumb text substitution performed by the preprocessor. It ignores scope, has no type verification, and is invisible to GDB symbolic debuggers.

2. const vs constexpr

  • const: Read-only variable; may be evaluated at runtime.
  • constexpr: Guaranteed compile-time constant expression; evaluated by the compiler during build.

⚡ 4. Embedded Systems & Hardware Reality

1. Zero SRAM Overhead with constexpr

constexpr primitive values are embedded directly into assembly instructions as immediate operands (MOV R0, #64), consuming 0 bytes of SRAM and 0 memory read cycles.

💡 5. Production-Ready Embedded Refactoring

Replacing macros with type-safe constexpr definitions:

💡 Production-Ready Refactor
#include <cstdint>

// BAD (Legacy C Macro): No type, no scope, debugger blind
// #define MAX_VOLTAGE 3.3

// GOOD (Modern C++): Type-safe, scoped, compile-time verified
namespace HardwareConfig {
    constexpr float    MAX_VOLTAGE_V  = 3.3f;
    constexpr uint32_t SPI_BAUD_RATE  = 10'000'000; // 10 MHz
    constexpr uint8_t  MAX_RETRY_COUNT = 3;
}

📝 Knowledge Verification Quiz

Test your understanding of the C++ concepts and embedded microcontroller trade-offs covered in this guide. Click any option for instant feedback.

Q1. Why is 'constexpr uint32_t MAX_SIZE = 128;' superior to '#define MAX_SIZE 128'?
A It is type-safe, respects C++ namespace scoping, and provides symbolic debugging information in GDB
B It executes in half the clock cycles
C It allows modifying the constant at runtime
D It allocates constants on the heap
Detailed Explanation: constexpr offers strong type checking, respects namespace encapsulation, and produces debug symbols for GDB.
Q2. Where are 'static constexpr' lookup tables placed in the microcontroller memory map?
A In Flash ROM (.rodata section), consuming 0 bytes of SRAM
B In SRAM (.bss section)
C On the CPU stack frame
D In the heap
Detailed Explanation: The linker places static constexpr tables in the read-only data (.rodata) section in Flash ROM.
Q3. What happens if a macro '#define SQUARE(x) x * x' is called as 'SQUARE(2 + 3)'?
A It expands to '2 + 3 * 2 + 3' which evaluates to 11 instead of 25 due to operator precedence bugs
B It evaluates to 25 correctly
C It causes a compile error
D It creates a runtime exception
Detailed Explanation: Macros perform raw text substitution; without parenthesis (x) * (x), operator precedence produces 2 + 6 + 3 = 11.
Q4. Can a constexpr variable be modified after its definition?
A No, constexpr implies const and is strictly immutable
B Yes, using const_cast
C Yes, in debug mode
D Yes, if declared inside a class
Detailed Explanation: constexpr guarantees immutability; values cannot be changed after definition.