Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 51

Understanding `const` and Its Removal

Recommended resources

Hello! Welcome to the final lesson in our module on type qualifiers and const correctness.

In our previous lessons, you've mastered const, volatile, and restrict, including how to combine them to precisely describe memory in an embedded context. We've treated const as a contract—a promise that your code will not modify a piece of data. This lesson is about what happens when that contract is deliberately broken.

Our learning outcome is to analyze the risks and appropriate use cases for casting away const. This is a powerful but perilous technique. Given your work in functional safety and with tools like Coverity/QAC, understanding the deep implications of this action is crucial for writing robust and compliant code.

The Great Danger: Modifying a const Object is Undefined Behavior

Let's be direct: if a variable is originally declared as const, any attempt to modify it—even after casting away the const qualifier—results in Undefined Behavior (UB).

In C, you can cast away const using a standard type cast:

const int a = 10;
int *p = (int *)&a; // Casting away const
*p = 20;            // Attempting to modify a const object -> UNDEFINED BEHAVIOR

This might seem to work sometimes, but it can fail in spectacular and non-obvious ways due to compiler optimizations. A const object might be placed in read-only memory, or the compiler might replace all reads of the variable with its initial value.

To see this in action, let's watch a short video that demonstrates the consequences. Although it uses C++'s const_cast, the underlying principle is identical in C.

C++ Weekly - Ep 283 - Stop Using const_cast!

The video 'C++ Weekly - Ep 283 - Stop Using const_cast!' by Jason Turner provides a crystal-clear demonstration of why modifying an originally-const object leads to undefined behavior. Pay close attention to how the compiler's optimizations render the modification ineffective.

Watch from the beginning (0:48) to 3:05. The presenter shows code that casts away const, writes a new value, but the program still returns the original const value. This is a direct result of the compiler leveraging the rules of undefined behavior.

Why Does This Happen? The "Assumed Absence of UB"

The behavior you just saw isn't a bug; it's a feature of high-performance compilers. Compilers operate on the principle of "assumed absence of undefined behavior." They assume your code is well-behaved and will never trigger UB. This assumption allows for powerful optimizations.

When you declare const int a = 10;, you give the compiler permission to:

  1. Replace every use of a directly with the value 10.
  2. Place a in a read-only data segment of the executable, which the operating system may protect from writes. Attempting to write to it would then cause a hardware fault (a segmentation fault on Linux or an access violation on Windows).

Casting away const and writing to *p violates the compiler's foundational assumption. Because the standard says the result is "undefined," the compiler is free to do anything—including producing code that appears to ignore your write completely.

The following video provides an excellent deep dive into how compilers exploit UB for optimization. Understanding this will fundamentally change how you view C code.

Advanced C: The UB and optimizations that trick good programmers.

The video 'Advanced C: The UB and optimizations that trick good programmers' by Eskil Steenberg is a masterclass in compiler behavior. We'll watch a section that explains how the 'assumed absence of UB' allows compilers to optimize away code that seems logically necessary.

Watch the segment from 19:35 to 27:32. Focus on the core idea: the compiler assumes UB is unreachable and can therefore eliminate code paths that would lead to it. This directly explains why modifying a const object is so unpredictable.


The "Legitimate" (But Still Risky) Use Cases

So, if modifying an originally const object is UB, is there ever a valid reason to cast away const? There is one, narrow, well-defined scenario, but it is often confused with a more common, illegitimate one.

Illegitimate Use Case: Interfacing with "Bad" APIs

The most common excuse for casting away const is to interface with a legacy or poorly designed function that fails to use const correctly in its parameters.

Imagine a library function:
void process_data(char *s1, size_t len);

The documentation swears that process_data only reads from s1. However, your code has a const char*.

const char* my_data = "Some important configuration string";
// process_data(my_data, strlen(my_data)); // This would cause a compiler warning/error.

The "quick fix" is to cast away const:
process_data((char*)my_data, strlen(my_data));

This is extremely risky.

  • You are overriding a compile-time safety check based on a promise in the documentation.
  • A future version of the library could change process_data to actually modify s1, and your code would silently break, possibly with UB if my_data points to a string literal in read-only memory.

The SEI CERT C standard explicitly warns against this. Failing to declare an unchanging parameter as const is a defect in the API, and casting to work around it is a violation of secure coding principles.

DCL13-C. Declare function parameters that are pointers ...

The SEI CERT C guideline 'DCL13-C' discusses the importance of const-correctness in function parameters. It highlights the dangers of APIs that are not const-correct.

Read the 'Noncompliant Code Example' section featuring strcat_nc and the 'Risk Assessment' section. This shows how an API's lack of const-correctness leads to problems and explicitly states that casting away const to bypass this violates another safety rule.

The proper solution is to fix the API to be const-correct. If that's not possible, this workaround should be heavily documented and understood as a significant source of technical debt and risk.

The Only Well-Defined Use Case

The C standard permits you to cast away const and modify the underlying object if and only if the original object was not declared const.

This sounds circular, so let's clarify with an example.

#include <stdio.h>

// This function only has a const "view" of the data
void try_to_modify(const int* p) {
    int* non_const_p = (int*)p; // Cast away const
    *non_const_p = 999;         // Is this UB? It depends!
}

int main() {
    // Case 1: Original object IS NOT const
    int x = 10;
    printf("Before: x = %d\n", x);
    try_to_modify(&x); // Perfectly valid and well-defined.
    printf("After: x = %d\n", x); // Will print 999

    // Case 2: Original object IS const
    const int y = 20;
    printf("Before: y = %d\n", y);
    try_to_modify(&y); // UNDEFINED BEHAVIOR
    printf("After: y = %d\n", y); // Could print 20, 999, or crash.

    return 0;
}

In Case 1, we start with a mutable int x. We pass its address to a function that receives it as a const int*. The const here is just a temporary, read-only "view." Inside the function, casting away that const view to modify the original, non-const x is perfectly legal.

In Case 2, we start with a const int y. When we try the same operation, we invoke UB because the object itself was immutable from its creation.

This well-defined use case is almost exclusively for situations where you have a non-const object but must pass it through a generic or intermediate API that (perhaps unnecessarily) applies a const view, before finally passing it to a function that needs write access.

Test your understanding!

Consider the following snippet. You are reviewing this code for a critical AUTOSAR component.

// This function stores a pointer to a configuration block for later use.
// The pointer itself is stored in volatile memory, as an ISR might update it.
static const uint8_t* g_active_config = NULL;

void set_active_config(const uint8_t* config_block) {
    g_active_config = config_block;
}

void apply_calibration_offset(uint16_t offset) {
    if (g_active_config != NULL) {
        // A specific field at byte 4 needs to be updated with the offset.
        // This field is known to be a uint16_t.
        uint16_t* calibration_field = (uint16_t*)(g_active_config + 4);
        *calibration_field += offset;
    }
}

int main() {
    uint8_t config_a[32] = {0}; // A mutable config block
    const uint8_t config_b[32] = {1}; // A read-only, default config block

    // Scenario 1
    set_active_config(config_a);
    apply_calibration_offset(100);

    // Scenario 2
    set_active_config(config_b);
    apply_calibration_offset(200);
}

Which scenario, if any, invokes Undefined Behavior, and why?

Show answer

Scenario 2 invokes Undefined Behavior.

  • Scenario 1 is well-defined. The original object, config_a, is a non-const array. set_active_config stores a const uint8_t* view of it. When apply_calibration_offset casts away const to modify the data, it is modifying the original, mutable config_a array. This is the one sanctioned use case for casting away const.

  • Scenario 2 is Undefined Behavior. The original object, config_b, is a const array. It may be stored in read-only memory. When apply_calibration_offset casts away const and attempts to write to it, it is attempting to modify an object that was originally defined as const. This is a direct violation and triggers UB. The program could crash with a hardware fault or behave unpredictably.


Conclusion: A Tool of Last Resort

We've seen that casting away const is one of the sharpest edges in the C language. It breaks the compiler's trust and opens the door to severe, hard-to-debug errors.

Key Takeaways:

  • Attempting to modify an object that was originally defined as const is Undefined Behavior, full stop. The compiler is free to make your code fail in non-obvious ways.
  • The only well-defined use of casting away const is to regain write access to an object that was not originally const, but which you only have a const pointer/view of.
  • The most common reason people reach for this tool—interfacing with a poorly-designed, non-const-correct API—is highly risky. It violates safety principles (like MISRA-C Rule 11.8) and creates fragile code. The correct fix is to improve the API's const correctness.
  • In your embedded work, this can lead to hardware faults if you attempt to write to memory sections the linker has placed in read-only flash/ROM.

Preview of the Next Module:
This lesson concludes our deep dive into type qualifiers. We've focused on using the type system to create safe and explicit contracts. In our next module, "Advanced Pointer Applications and Performance," we'll start by exploring the opposite: void pointers. We will learn how to use void* for generic programming, a technique that relinquishes compile-time type safety in exchange for flexibility, and analyze the trade-offs involved.