Project 13: C23 Modern Features Laboratory

A hands-on lab exploring C23 language features, safer arithmetic APIs, and modern attributes.

Quick Reference

Attribute Value
Difficulty Level 3 - Advanced
Time Estimate 2-3 weeks
Main Programming Language C
Alternative Programming Languages None
Coolness Level Level 4 - Hardcore Tech Flex
Business Potential Level 1 - Resume Gold
Prerequisites C11/C17 familiarity, compiler setup
Key Topics C23 features, checked arithmetic, attributes

1. Learning Objectives

By completing this project, you will:

  1. Use C23 checked arithmetic APIs to prevent overflow.
  2. Explore modern language features like nullptr and static_assert keyword.
  3. Apply standard attributes ([[nodiscard]], [[deprecated]], [[maybe_unused]]).
  4. Evaluate compiler support and build feature-detection helpers.
  5. Produce a compatibility report across compilers and flags.

2. All Theory Needed (Per-Concept Breakdown)

Concept 1: Checked Arithmetic with stdckdint.h

Fundamentals

C23 introduces checked arithmetic functions in <stdckdint.h> such as ckd_add, ckd_sub, and ckd_mul. These functions perform arithmetic and report overflow explicitly, rather than invoking undefined behavior for signed overflow. This makes it possible to write safe, portable arithmetic code without relying on compiler-specific builtins.

Deep Dive into the concept

Prior to C23, safe arithmetic typically relied on manual bounds checking, wider types, or compiler intrinsics (__builtin_add_overflow). These approaches were either verbose or non-portable. C23 standardized checked arithmetic functions to solve this. The ckd_* functions take pointers to output variables and return a boolean indicating whether overflow occurred. This design avoids undefined behavior because the operation is defined to either store the correct result or indicate overflow.

Using checked arithmetic requires careful attention to types. ckd_add operates on integer types and the type of the output pointer controls the result type. If you pass a short* output but the inputs are int, you may get narrowing or different overflow semantics. The best practice is to use an output type at least as wide as the inputs. In performance-sensitive code, the overhead is small because compilers can inline these operations and use hardware flags when available.

Another important aspect is that checked arithmetic helps document intent. Instead of relying on implicit overflow or manual checks, you use an explicit API that signals the possibility of overflow. This is valuable in security-sensitive code and input validation logic. In this project, you will build a set of arithmetic utilities that wrap ckd_* and compare them to traditional approaches, documenting performance and portability.

To deepen this section, include guidance on fallback implementations for compilers that lack stdckdint.h. You can wrap compiler-specific intrinsics (__builtin_add_overflow) behind a compatibility layer so callers always use the same API. This also lets you unit test checked arithmetic in a compiler-agnostic way. Another detail is that checked arithmetic is useful not only for overflow detection but also for building safe size computations in memory allocation. For example, when computing count times size, you can use ckd_mul to prevent integer overflow that would cause under-allocation. This directly ties into secure coding practices and demonstrates a real-world use case where checked arithmetic prevents memory corruption.

To operationalize this concept in a real codebase, create a short checklist of invariants and a set of micro-experiments. Start with a minimal, deterministic test that isolates one rule or behavior, then vary a single parameter at a time (inputs, flags, platform, or data layout) and record the outcome. Keep a table of assumptions and validate them with assertions or static checks so violations are caught early. Whenever the concept touches the compiler or OS, capture tool output such as assembly, warnings, or system call traces and attach it to your lab notes. Finally, define explicit failure modes: what does a violation look like at runtime, and how would you detect it in logs or tests? This turns abstract theory into repeatable engineering practice and makes results comparable across machines and compiler versions.

Another way to deepen understanding is to map the concept to a small decision table: list inputs, expected outcomes, and the assumptions that must hold. Create at least one negative test that violates an assumption and observe the failure mode, then document how you would detect it in production. Add a short trade-off note: what you gain by following the rule and what you pay in complexity or performance. Where possible, instrument the implementation with debug-only checks so violations are caught early without affecting release builds. If the concept admits multiple approaches, implement two and compare them; the act of measuring and documenting the difference is part of professional practice. This habit turns theoretical understanding into an engineering decision framework you can reuse across projects.

How this fits on projects

Definitions & key terms

  • Checked arithmetic: Arithmetic operations that report overflow explicitly.
  • ckd_add/ckd_sub/ckd_mul: C23 checked arithmetic APIs.
  • Overflow flag: Boolean result indicating overflow occurred.
  • Narrowing: Assigning a value to a smaller type.

Mental model diagram (ASCII)

ckd_add(&out, a, b) -> returns true if overflow else false

How it works (step-by-step, with invariants and failure modes)

  1. Provide inputs and output pointer.
  2. Function performs operation and checks overflow.
  3. On success, output is stored and function returns false.
  4. On overflow, output is unspecified or clamped, return true.

Invariant: Overflow is reported without invoking UB. Failure mode: Using mismatched types can cause unexpected truncation.

Minimal concrete example

#include <stdckdint.h>
int out;
if (ckd_add(&out, a, b)) {
    // handle overflow
}

Common misconceptions

  • “Checked arithmetic is slower by default.” → Compilers often inline it.
  • “It works for floating point.” → It is for integer types only.
  • “It replaces all bounds checks.” → You still need semantic checks.

Check-your-understanding questions

  1. What does ckd_add return on overflow?
  2. Why is checked arithmetic safer than manual overflow checks?
  3. What type should the output pointer use?
  4. Can ckd_mul prevent UB for signed overflow?
  5. How does checked arithmetic improve portability?

Check-your-understanding answers

  1. It returns true (overflow occurred).
  2. It avoids UB and is standardized in C23.
  3. A type at least as wide as the inputs.
  4. Yes, it reports overflow instead of invoking UB.
  5. It avoids compiler-specific intrinsics.

Real-world applications

  • Parsing user inputs with bounds guarantees.
  • Financial calculations where overflow must be detected.

Where you’ll apply it

References

  • C23 <stdckdint.h> specification
  • Compiler documentation on checked arithmetic support

Key insights

Checked arithmetic brings defined overflow handling into the C standard.

Summary

C23’s checked arithmetic APIs allow overflow detection without UB, making arithmetic code safer and more portable.

Homework/Exercises to practice the concept

  1. Replace manual overflow checks with ckd_add in a function.
  2. Compare performance of ckd_add vs __builtin_add_overflow.
  3. Create a test suite of boundary values.

Solutions to the homework/exercises

  1. Use ckd_add and return a boolean error code.
  2. Performance should be similar when inlined.
  3. Include INT_MAX, INT_MIN, and mid-range values.

Concept 2: Modern C23 Syntax and Attributes

Fundamentals

C23 standardizes several modern features that improve safety and readability. These include the nullptr constant, static_assert keyword, and standardized attributes such as [[nodiscard]], [[deprecated]], and [[maybe_unused]]. While compiler support varies, these features provide clearer intent and stronger diagnostics.

Deep Dive into the concept

The nullptr constant provides a type-safe null pointer constant, similar to C++’s nullptr. It avoids ambiguities of NULL and 0, especially in generic macros or overloaded contexts (if combined with _Generic). The static_assert keyword replaces _Static_assert with a more consistent syntax, allowing compile-time checks that fail the build when invariants are violated. For example, you can assert that a struct has a specific size or that an enum fits in a type.

Attributes in C23 standardize compiler hints that were previously compiler-specific. [[nodiscard]] warns when a return value is ignored; [[deprecated]] warns when an API is used; [[maybe_unused]] suppresses warnings for intentionally unused variables. These attributes improve code clarity and help maintainers understand intent. However, support is compiler-dependent, so your code should use feature detection (e.g., __has_c_attribute) to enable attributes when available and fall back gracefully otherwise.

Some compilers also support additional C23 features like #embed for including binary data and typeof/typeof_unqual for type introspection. These features are optional and may not be implemented uniformly. The lab should document which compilers support each feature and provide fallback implementations. The key lesson is that modern C features are not automatically portable; you must check support and build guardrails.

In this project, you will create a “C23 feature matrix” by compiling sample programs under GCC and Clang with -std=c23. You will use feature macros to detect support and conditionally enable features. This will give you a practical understanding of how language evolution affects real-world codebases.

To operationalize this concept in a real codebase, create a short checklist of invariants and a set of micro-experiments. Start with a minimal, deterministic test that isolates one rule or behavior, then vary a single parameter at a time (inputs, flags, platform, or data layout) and record the outcome. Keep a table of assumptions and validate them with assertions or static checks so violations are caught early. Whenever the concept touches the compiler or OS, capture tool output such as assembly, warnings, or system call traces and attach it to your lab notes. Finally, define explicit failure modes: what does a violation look like at runtime, and how would you detect it in logs or tests? This turns abstract theory into repeatable engineering practice and makes results comparable across machines and compiler versions.

Another way to deepen understanding is to map the concept to a small decision table: list inputs, expected outcomes, and the assumptions that must hold. Create at least one negative test that violates an assumption and observe the failure mode, then document how you would detect it in production. Add a short trade-off note: what you gain by following the rule and what you pay in complexity or performance. Where possible, instrument the implementation with debug-only checks so violations are caught early without affecting release builds. If the concept admits multiple approaches, implement two and compare them; the act of measuring and documenting the difference is part of professional practice. This habit turns theoretical understanding into an engineering decision framework you can reuse across projects.

How this fits on projects

Definitions & key terms

  • nullptr: A type-safe null pointer constant in C23.
  • static_assert: Compile-time assertion keyword.
  • Attributes: Standardized annotations like [[nodiscard]].
  • Feature detection: Checking support at compile time.

Mental model diagram (ASCII)

#if __has_c_attribute(nodiscard)
#define NODISCARD [[nodiscard]]
#else
#define NODISCARD
#endif

How it works (step-by-step, with invariants and failure modes)

  1. Detect feature support with macros.
  2. Define portable wrappers (e.g., NODISCARD).
  3. Use features where supported.
  4. Fall back cleanly when unsupported.

Invariant: Code compiles with or without C23 support. Failure mode: Using unsupported features breaks builds.

Minimal concrete example

static_assert(sizeof(void*) == 8, "Expect 64-bit pointers");

Common misconceptions

  • “All compilers fully support C23.” → Support is partial and evolving.
  • “Attributes are only stylistic.” → They can enforce diagnostics.
  • nullptr is just NULL.” → It is a distinct type-safe constant.

Check-your-understanding questions

  1. What problem does nullptr solve?
  2. Why use static_assert?
  3. How do you enable attributes portably?
  4. What happens if a feature isn’t supported?
  5. Why build a feature matrix?

Check-your-understanding answers

  1. It provides a type-safe null pointer constant.
  2. It enforces compile-time invariants.
  3. Use __has_c_attribute and fallback macros.
  4. Compilation fails unless you guard it.
  5. To understand compiler support and portability.

Real-world applications

  • Safer APIs with [[nodiscard]] return values.
  • Portable compile-time checks for data structures.

Where you’ll apply it

References

  • C23 draft feature lists
  • Compiler docs for __has_c_attribute

Key insights

Modern C features improve safety, but only if you detect support and provide fallbacks.

Summary

C23 adds useful syntax and attributes that improve diagnostics and safety, but portability demands feature detection and careful fallback strategies.

Homework/Exercises to practice the concept

  1. Create a NODISCARD macro that works on GCC and Clang.
  2. Write a static_assert for struct size.
  3. Build a small feature detection header.

Solutions to the homework/exercises

  1. Use __has_c_attribute or compiler-specific macros.
  2. Place static_assert(sizeof(MyType) == expected, ...).
  3. Centralize detection in c23_features.h.

3. Project Specification

3.1 What You Will Build

A laboratory of C23 features with small demo programs, a compatibility matrix across compilers, and a reusable feature-detection header for production code.

3.2 Functional Requirements

  1. Feature demos: Checked arithmetic, nullptr, static_assert, attributes.
  2. Feature detection: Header that maps support to macros.
  3. Compatibility matrix: GCC/Clang support table.
  4. Fallbacks: Equivalent implementations when unsupported.
  5. Documentation: Clear notes on portability and limitations.

3.3 Non-Functional Requirements

  • Performance: Demos run in under 10 seconds.
  • Reliability: Deterministic outputs for all demos.
  • Usability: Single command to run all demos.

3.4 Example Usage / Output

$ ./c23_lab --feature ckd_add
ckd_add: overflow detected -> true

3.5 Data Formats / Schemas / Protocols

Matrix format (Markdown):

Feature | GCC | Clang | Notes
ckd_add | yes | yes | requires <stdckdint.h>

3.6 Edge Cases

  • Missing <stdckdint.h> header.
  • Partial attribute support.
  • Compiler flags not enabling C23.

3.7 Real World Outcome

What you will see:

  1. A set of C23 demo programs.
  2. A compatibility matrix across compilers.
  3. A reusable feature-detection header.

3.7.1 How to Run (Copy/Paste)

make
./c23_lab --all

3.7.2 Golden Path Demo (Deterministic)

Run ckd_add demo with fixed inputs and verify overflow detection.

3.7.3 If CLI: exact terminal transcript

$ ./c23_lab --feature ckd_add
Input: 1000000 + 2000000
Overflow: false
Result: 3000000
Exit: 0

Failure demo (deterministic):

$ ./c23_lab --feature unknown
ERROR: unsupported feature
Exit: 2

4. Solution Architecture

4.1 High-Level Design

+-------------------+
| feature header     |
+---------+---------+
          |
          v
+-------------------+     +-------------------+
| demo programs      | -->| compatibility matrix |
+-------------------+     +-------------------+

4.2 Key Components

| Component | Responsibility | Key Decisions | |———–|—————-|—————-| | Feature header | Detect support | Use macros + fallbacks | | Demo programs | Showcase features | Small, deterministic | | Matrix generator | Summarize support | Markdown output |

4.3 Data Structures (No Full Code)

#define HAS_CKD 1

4.4 Algorithm Overview

  1. Detect feature support via macros.
  2. Compile demo programs.
  3. Run demos and collect results.

Complexity Analysis:

  • Time: O(F) for feature demos
  • Space: O(1)

5. Implementation Guide

5.1 Development Environment Setup

clang -std=c23 -Wall -Wextra -Werror -g

5.2 Project Structure

c23-lab/
├── include/
│   └── c23_features.h
├── src/
│   ├── demo_ckd.c
│   ├── demo_attrs.c
│   └── main.c
├── docs/
│   └── matrix.md
└── Makefile

5.3 The Core Question You’re Answering

“Which C23 features are useful today, and how do I use them safely?”

5.4 Concepts You Must Understand First

  1. Checked arithmetic APIs.
  2. Feature detection and fallbacks.
  3. Standard attributes and nullptr.

5.5 Questions to Guide Your Design

  1. How will you detect feature support without breaking builds?
  2. Which demos best show real-world usage?
  3. How will you document partial compiler support?

5.6 Thinking Exercise

Design a macro that enables [[nodiscard]] only when supported.

5.7 The Interview Questions They’ll Ask

  1. Why is checked arithmetic important?
  2. How do you ensure portability with new features?
  3. What is the purpose of static_assert?

5.8 Hints in Layers

  • Hint 1: Start with ckd_add and static_assert.
  • Hint 2: Add attribute macros.
  • Hint 3: Build a compiler support matrix.

5.9 Books That Will Help

| Topic | Book | Chapter | |——-|——|———| | Modern C | “Effective C” — Seacord | New features sections |

5.10 Implementation Phases

Phase 1: Foundation (4-5 days)

  • Build feature detection header.
  • Checkpoint: Header compiles on GCC and Clang.

Phase 2: Core Functionality (5-6 days)

  • Add demos for ckd_* and attributes.
  • Checkpoint: Demos run deterministically.

Phase 3: Polish & Edge Cases (3-4 days)

  • Generate compatibility matrix.
  • Checkpoint: Matrix documents support and caveats.

5.11 Key Implementation Decisions

| Decision | Options | Recommendation | Rationale | |———-|———|—————-|———–| | Feature detection | macros, configure step | macros | Simpler setup | | Matrix output | text, Markdown | Markdown | Easy to read |


6. Testing Strategy

6.1 Test Categories

| Category | Purpose | Examples | |———|———|———-| | Unit tests | Validate feature macros | compile tests | | Integration tests | Run demos | --all | | Edge case tests | Missing headers | fallback paths |

6.2 Critical Test Cases

  1. ckd_add detects overflow.
  2. static_assert fires on invalid assumptions.
  3. Attribute macros compile on older compilers.

6.3 Test Data

Input: INT_MAX + 1 (overflow)

7. Common Pitfalls & Debugging

7.1 Frequent Mistakes

| Pitfall | Symptom | Solution | |——–|———|———-| | Assuming feature support | Build errors | Use detection macros | | Mixing old and new syntax | Warnings | Standardize on C23 mode | | Ignoring fallbacks | Non-portable code | Provide alternatives |

7.2 Debugging Strategies

  • Compile with -std=c23 and -Winvalid-pch.
  • Use -E to inspect feature macros.

7.3 Performance Traps

Overusing attribute macros can clutter code; centralize in headers.


8. Extensions & Challenges

8.1 Beginner Extensions

  • Add a demo for nullptr vs NULL.

8.2 Intermediate Extensions

  • Add #embed demo if supported by your compiler.

8.3 Advanced Extensions

  • Create a portable header that backports C23 features to C11.

9. Real-World Connections

9.1 Industry Applications

  • Safer arithmetic in financial systems.
  • Cleaner diagnostics in large codebases.
  • libc implementations adopting C23 features.

9.3 Interview Relevance

  • Questions about modern C evolution and safe arithmetic.

10. Resources

10.1 Essential Reading

  • C23 draft feature summaries
  • Compiler documentation for C23 support

10.2 Video Resources

  • Talks on modern C features

10.3 Tools & Documentation

  • GCC/Clang C23 release notes

11. Self-Assessment Checklist

11.1 Understanding

  • I can explain checked arithmetic APIs.
  • I can use static_assert and attributes.
  • I can detect compiler support for features.

11.2 Implementation

  • Feature demos run on my compiler.
  • Compatibility matrix is accurate.
  • Fallbacks compile under C11.

11.3 Growth

  • I can introduce C23 features safely in a real project.
  • I can document compiler support clearly.

12. Submission / Completion Criteria

Minimum Viable Completion:

  • Feature detection header.
  • Demos for checked arithmetic and attributes.
  • Compatibility matrix.

Full Completion:

  • All minimum criteria plus:
  • Fallbacks for older compilers.

Excellence (Going Above & Beyond):

  • Backport library that emulates C23 features in C11.