const vs constexpr in C++ Explained with Embedded Examples

When developing firmware, performance, memory footprint, and safety are critical. That is where keywords like const and constexpr come into play. At first glance, both seem to make variables immutable, but they differ significantly in when and how they achieve this.

In this post, I will explain the technical depth behind const and constexpr, their purpose, key differences, use cases, and how choosing the right one impacts performance and reliability, especially in embedded systems like STM32, AVR, or ARM Cortex-M series.

 

What is const?

The const keyword in C and C++ makes a variable read-only after initialization. It simply means: “this value must not change once set.”

✅ Syntax:

const int baudRate = 9600;

Once initialized, baudRate can’t be changed. But—note this carefully—it doesn’t guarantee that its value is known at compile time.

 

What is constexpr?

Introduced in C++11, constexpr takes immutability a step further. It tells the compiler: “I want this value and the computation producing it to be resolved at compile-time.”

constexpr int maxBuffer = 128;

If the expression assigned to a constexpr variable is not computable at compile-time, the compiler throws an error.

 

const vs constexpr: Core Difference

Feature const constexpr
Meaning Read-only after initialization Compile-time constant
Evaluated Can be at runtime Must be compile-time
Array size usage May not work Always works (post-C++11)
Function restriction None Must be constexpr function if used
Use in templates Limited Allowed

 

📌 Real Firmware Use Case:

Just Imagine you are working on a project in which UART initialization is required:

//Initialization

const uint32_t uartBaudRate = getBaudRateFromUserApp();

constexpr uint32_t defaultBaud = 115200;
  • uartBaudRate: Value comes from EEPROM at runtime. It must be const.
  • defaultBaud: Fixed compile-time default. constexpr helps store this in ROM and allows better optimization.

 

Compile-Time vs Runtime Evaluation:

✅ const (Can Be Runtime):

int readFromSensor();

// ✅ Allowed, but runtime evaluated
const int threshold = readFromSensor(); 

 

✅ constexpr (Must Be Compile-Time):

constexpr int bufferSize = 1024; // ✅ OK
constexpr int size = readFromSensor(); // ❌ Error: not a constexpr function

 

✨ constexpr Functions:

Another gem of modern C++ is constexpr functions—functions that can be evaluated at compile time if passed compile-time values.

constexpr int square(int x) 
{
    return x * x;
}


 // ✅ evaluated at compile time
constexpr int result = square(5);

This helps in micro-optimizing lookup tables, constants, or configuration data.

 

🧪 Embedded Insight: ROM vs RAM:

Choosing constexpr often pushes data into Flash/ROM rather than RAM, saving precious memory on small MCUs.

const int data1= 10;        // Might go to .data section

constexpr int data2= 10;    // Goes to .rodata or Flash (usually)

 

🧰 Common Mistakes and Tips:

 

✅ Prefer constexpr for hardware settings:

constexpr uint32_t F_CPU = 16000000UL;

constexpr uint32_t TICK_US = 1000;

 

✅ Use constexpr for buffer size:

const int size = 10;


int buffer[size];

 

❌ Misuse of const expecting compile-time constant:

const int size = getRuntimeValue();

int buffer[size];  // ❌ Not valid in pre-C++14

 

Recommended Post: