Skip to content

Introduction

Callbacks are often necessary when programming embedded systems. In C, callbacks are usually implemented as function pointers. A function pointer is typically defined as follows:

typedef void (*fun_ptr)()
One can then define a function with the same signature and pass it as a callback to another function that takes a function pointer as an argument. With this approach, a different signature must be defined for each type of callback. For example, if we need to define a function that returns an integer and takes an integer array as a parameter, the following function pointer should be defined:
typedef int (*fun_ptr)(int*)
To avoid redefining function pointers for each specific case, arguments are often defined as void*. This allows one to define more generic callback functions at the cost of removing type-specific casting. The compiler will not detect any errors made by the programmer regarding type mismatches. This is

Using Function Pointers

The use of function pointers and context passed as void* is implemented with code such as:

// C style — pointer to function taking void* context
typedef void (*callback_t)(void *ctx);

void register_callback(callback_t cb, void *ctx);

// Usage
void my_handler(void *ctx, int event) {
    my_state_t *s = (my_state_t *)ctx;
    // use s
}
register_callback(my_handler, &my_state);

An example of the use of function pointers is given below:

src/callback_fp.c
src/callback_fp.c
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file callback_fp.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief callback with function pointer implementation
 *        Classic C callback pattern: function pointer + void* context.
 *
 * @date 2025-07-01
 * @version 1.0.0
 ***************************************************************************/

#include "callback_fp.h"

// zephyr
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>

// std
#include <stdio.h>
#include <stdlib.h>

LOG_MODULE_DECLARE(programming, CONFIG_APP_LOG_LEVEL);

// ── Types ─────────────────────────────────────────────────────────────────────
typedef enum {
  EVENT_NONE     = 0,
  EVENT_PRESSED  = 1,
  EVENT_RELEASED = 2,
} event_t;

typedef struct {
  int id;
  unsigned press_count;
  unsigned release_count;
} button_state_t;

// Callback signature: takes opaque context pointer and event code
typedef void (*callback_t)(void* ctx, int event);

// ── Callback registry ─────────────────────────────────────────────────────────
typedef struct {
  callback_t cb;
  void* ctx;
} registry_entry_t;

#define MAX_CALLBACKS 8

static registry_entry_t registry[MAX_CALLBACKS];
static int registry_count = 0;

static void register_callback(callback_t cb, void* ctx) {
  if (registry_count >= MAX_CALLBACKS) {
    LOG_ERR("ERROR: callback registry full");
    return;
  }
  registry[registry_count].cb  = cb;
  registry[registry_count].ctx = ctx;
  registry_count++;
}

static void dispatch_event(int event) {
  for (int i = 0; i < registry_count; i++) {
    if (registry[i].cb) {
      registry[i].cb(registry[i].ctx, event);
    }
  }
}

// ── Callbacks ─────────────────────────────────────────────────────────────────

// Handler for button A
static void button_handler(void* ctx, int event) {
  // ctx must be cast back to the concrete type — not type safe
  button_state_t* state = (button_state_t*)ctx;

  switch ((event_t)event) {
    case EVENT_PRESSED:
      state->press_count++;
      LOG_INF("[button %d] pressed  (total: %u)", state->id, state->press_count);
      break;
    case EVENT_RELEASED:
      state->release_count++;
      LOG_INF("[button %d] released (total: %u)", state->id, state->release_count);
      break;
    default:
      break;
  }
}

// A second handler registered for the same events
static void logger_handler(void* ctx, int event) {
  const char* label = (const char*)ctx;
  LOG_INF("[logger '%s'] event=%d", label, event);
}

// Function called from main()
void callback_fp(void) {
  button_state_t btn_a = {.id = 0, .press_count = 0, .release_count = 0};
  button_state_t btn_b = {.id = 1, .press_count = 0, .release_count = 0};

  // Register handlers — each carries its own context via void*
  register_callback(button_handler, &btn_a);
  register_callback(button_handler, &btn_b);
  register_callback(logger_handler, (void*)"audit");

  // Simulate events
  dispatch_event(EVENT_PRESSED);
  dispatch_event(EVENT_PRESSED);
  dispatch_event(EVENT_RELEASED);

  LOG_INF("callback_fp: Final counts: btn_a pressed=%u btn_b pressed=%u",
          btn_a.press_count,
          btn_b.press_count);

  // Registering the following callbacks is valid but may cause a crash
  // This shows that type-unsafe cast are possible
  // int* a = NULL;
  // register_callback(button_handler, a);
  // register_callback(logger_handler, &a);
  // dispatch_event(EVENT_RELEASED);
}
src/callback_fp.h
src/callback_fp.h
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file callback_fp.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief callback with function pointer declaration
 *
 * @date 2025-07-01
 * @version 1.0.0
 ***************************************************************************/

#pragma once

#ifdef __cplusplus
extern "C" {
#endif

void callback_fp();

#ifdef __cplusplus
}
#endif

This example uses the classic C function pointer pattern, in which the context is passed as void*. Using void* implies an unsafe cast inside callback handlers.

Replace void* Context with a Capturing Lambda

In C++, it is possible to type the context and to avoid the void* pointer and related unsafe casts. An elegant solution is to pass a lambda with a capture list that replaces the function pointer and void* pair cleanly:

// C++ style — lambda captures state directly, no void* needed
my_state_t state;

register_callback([&state](int event) {
    // state is captured by reference — no cast needed
    state.handle(event);
});
This solution can be used when the callback is stored for the lifetime of the object and is the right choice for many embedded programming use cases.

A more complete example is given below:

src/callback_lambda.cpp
src/callback_lambda.cpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file callback_lambda.cpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief callback with capturing lambda implementation
 *        As compared to callback_fp, replace void* context + function pointer with a
 *        capturing lambda. The capture list carries state — no cast needed.
 *
 * @date 2025-07-01
 * @version 1.0.0
 ***************************************************************************/

#include "callback_lambda.hpp"

// zephyr
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>

// std
#include <cstdio>
#include <functional>
#include <utility>

LOG_MODULE_DECLARE(programming, CONFIG_APP_LOG_LEVEL);

// ── Types ─────────────────────────────────────────────────────────────────────
enum class Event {
  None     = 0,
  Pressed  = 1,
  Released = 2,
};

struct ButtonState {
  int id                 = 0;
  unsigned press_count   = 0;
  unsigned release_count = 0;
};

// ── Callback registry ─────────────────────────────────────────────────────────
// Uses std::function here only to hold heterogeneous lambdas in the registry.
static constexpr int MAX_CALLBACKS = 8;

using Callback = std::function<void(Event)>;

static Callback registry[MAX_CALLBACKS];
static int registry_count = 0;

static void register_callback(Callback cb) {
  if (registry_count >= MAX_CALLBACKS) {
    LOG_ERR("ERROR: callback registry full");
    return;
  }
  registry[registry_count++] = std::move(cb);
}

static void dispatch_event(Event event) {
  for (int i = 0; i < registry_count; i++) {
    if (registry[i]) {
      registry[i](event);
    }
  }
}

void callback_lambda() {
  ButtonState btn_a{.id = 0};
  ButtonState btn_b{.id = 1};

  // ── Before: C style ───────────────────────────────────────────────────────
  // register_callback(button_handler, &btn_a);   // void* — unsafe cast inside
  //
  // ── After: capturing lambda ───────────────────────────────────────────────
  // The lambda captures btn_a by reference.
  // No void* parameter, no cast, full type safety.

  register_callback([&btn_a](Event e) {
    switch (e) {
      case Event::Pressed:
        btn_a.press_count++;
        LOG_INF("[button %d] pressed  (total: %u)", btn_a.id, btn_a.press_count);
        break;
      case Event::Released:
        btn_a.release_count++;
        LOG_INF("[button %d] released (total: %u)", btn_a.id, btn_a.release_count);
        break;
      default:
        break;
    }
  });

  register_callback([&btn_b](Event e) {
    switch (e) {
      case Event::Pressed:
        btn_b.press_count++;
        LOG_INF("[button %d] pressed  (total: %u)", btn_b.id, btn_b.press_count);
        break;
      case Event::Released:
        btn_b.release_count++;
        LOG_INF("[button %d] released (total: %u)", btn_b.id, btn_b.release_count);
        break;
      default:
        break;
    }
  });

  // A stateless lambda — equivalent to a plain function pointer, zero overhead
  register_callback([](Event e) { LOG_INF("[logger] event=%d", static_cast<int>(e)); });

  // Simulate events
  dispatch_event(Event::Pressed);
  dispatch_event(Event::Pressed);
  dispatch_event(Event::Released);

  LOG_INF("callback_lambda: Final counts: btn_a pressed=%u  btn_b pressed=%u",
          btn_a.press_count,
          btn_b.press_count);

  // The callback must be of type Callback (std::function<void(Event)>)
  // It is not possible to pass a callback of another type like
  // register_callback([](int e) {
  //
  //});
}
src/callback_lambda.hpp
src/callback_lambda.hpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file callback_lambda.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief callback with capturing lambda declaration
 *
 * @date 2025-07-01
 * @version 1.0.0
 ***************************************************************************/

#pragma once

void callback_lambda();

It’s important to note that stateless lambdas (those with no capture) are equivalent to plain function pointers with zero overhead but with type safety!

Zero-Cost Capturing Lambda with Template Parameters

When zero-cost alternatives are necessary and memory allocation is not permitted, a simple solution is to specify the callback type as a template parameter, as shown here:

enum class Event { Pressed = 0, Released = 1 };

template<typename CallbackType>
class ClassWithCallback {
public:
  explicit ClassWithCallback(CallbackType callback) : _cb(std::move(callback)) {}
  void simulateCB() {
    _cb(Event::Pressed);
  }
private:
  CallbackType _cb;
};

// declare a ClassWithCallback instance
ClassWithCallback class_with_cb([](Event e) { 
  // use e
});
class_w.simulateCB();
Using zero-cost template callbacks enables the compiler to identify the precise type of the callback and store the lambda by value. Consequently, the compiler can inline everything without virtual dispatch or heap allocation. A complete example is given below:

src/callback_lambda_template.cpp
src/callback_lambda_template.cpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file callback_lambda_template.cpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief callback with capturing lambda function implementation
 *        As compared to callback_lambda, the capturing lambda
 *        is passed to a template parameter. No std::function used anywhere.
 *        The lambda type is fully known at compile time and the compiler
 *        can inline the call. This is a zero-cost alternative.
 *
 * @date 2025-07-01
 * @version 1.0.0
 ***************************************************************************/

#include "callback_lambda_template.hpp"

// zephyr
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>

// std
#include <cstdint>
#include <cstdio>
#include <utility>

LOG_MODULE_DECLARE(programming, CONFIG_APP_LOG_LEVEL);

// ── Types ─────────────────────────────────────────────────────────────────────

enum class Event {
  None     = 0,
  Pressed  = 1,
  Released = 2,
};

struct ButtonState {
  int id                 = 0;
  unsigned press_count   = 0;
  unsigned release_count = 0;
};

// ── Hardware abstraction — template callback, no std::function ────────────────

/**
 * ButtonDriver stores the callback as a template parameter.
 * The lambda type is fully resolved at compile time.
 * No type erasure. No heap allocation. No virtual dispatch.
 *
 * In C this would be:
 *   void register_callback(callback_t cb, void *ctx);
 *   register_callback(my_handler, &my_state);
 *
 * Here the "context" is the lambda capture list — type safe, no cast needed.
 */
template <typename OnEvent>
class ButtonDriver {
 public:
  explicit ButtonDriver(int hw_id, OnEvent on_event)
      : _hw_id(hw_id), _on_event(std::move(on_event)) {}

  // Simulate hardware interrupt — calls callback directly, compiler inlines it
  void simulate_press() { _on_event(Event::Pressed); }
  void simulate_release() { _on_event(Event::Released); }

  int id() const { return _hw_id; }

 private:
  int _hw_id;
  OnEvent _on_event;  // concrete lambda type — NOT std::function
};

void callback_lambda_template() {
  ButtonState btn_a{.id = 0};
  ButtonState btn_b{.id = 1};

  // ── C style (before) ──────────────────────────────────────────────────────
  // void *ctx = &btn_a;
  // register_callback(button_handler, ctx);
  // Inside button_handler: ButtonState *s = (ButtonState *)ctx;  ← unsafe cast
  //
  // ── C++ style (after) — lambda captures state, no void*, no cast ─────────

  ButtonDriver driver_a(0, [&btn_a](Event e) {
    // btn_a is captured by reference — type safe, no cast
    switch (e) {
      case Event::Pressed:
        btn_a.press_count++;
        LOG_INF("[button %d] pressed  (total: %u)", btn_a.id, btn_a.press_count);
        break;
      case Event::Released:
        btn_a.release_count++;
        LOG_INF("[button %d] released (total: %u)", btn_a.id, btn_a.release_count);
        break;
      default:
        break;
    }
  });

  ButtonDriver driver_b(1, [&btn_b](Event e) {
    switch (e) {
      case Event::Pressed:
        btn_b.press_count++;
        LOG_INF("[button %d] pressed  (total: %u)", btn_b.id, btn_b.press_count);
        break;
      case Event::Released:
        btn_b.release_count++;
        LOG_INF("[button %d] released (total: %u)", btn_b.id, btn_b.release_count);
        break;
      default:
        break;
    }
  });

  // Simulate events
  driver_a.simulate_press();
  driver_b.simulate_press();
  driver_a.simulate_press();
  driver_a.simulate_release();

  LOG_INF("callback_lambda_template: Final counts: btn_a pressed=%u  btn_b pressed=%u",
          btn_a.press_count,
          btn_b.press_count);

  // In this example, ButtonDriver<lambda_type_A> and ButtonDriver<lambda_type_B>
  // have different types.
  // This approach is therefore not adequate when callbacks need to be stored
  // in the same array or vector (heterogeneous storage). In this
  // case std::function is needed, as in the callback_lambda implementation.
}
src/callback_lambda_template.hpp
src/callback_lambda_template.hpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file callback_lambda.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief callback with capturing lambda function declaration
 *        As compared to callback_lambda, the capturing lambda
 *        is passed to a template parameter. No std::function used anywhere.
 *        The lambda type is fully known at compile time and the compiler
 *        can inline the call. This is a zero-cost alternative.
 *
 * @date 2025-07-01
 * @version 1.0.0
 ***************************************************************************/

#pragma once

void callback_lambda_template();

Capturing Lambdas and Lifetime

When using capturing lambdas, developers must be very careful about the lifetime of objects captured. The code below demonstrates the following important concepts:

  • Avoid dangling reference from capturing a local by reference
  • Capture by value as the safe alternative
  • Use shared_ptr capture for shared ownership across callbacks
  • Use weak_ptr capture to avoid extending lifetime unintentionally
src/callback_lifetime.cpp
src/callback_lifetime.cpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file callback_lifetime.cpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief Demonstrates lifetime and capture discipline.
 *
 *   - Dangling reference from capturing a local by reference
 *   - Capture by value as the safe alternative
 *   - shared_ptr capture for shared ownership across callbacks
 *   - Weak_ptr capture to avoid extending lifetime unintentionally
 *
 *
 * @date 2025-07-01
 * @version 1.0.0
 ***************************************************************************/

#include "callback_lifetime.hpp"

// zephyr
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>

// std
#include <cstdio>
#include <functional>
#include <memory>
#include <string>
#include <utility>
#include <vector>

LOG_MODULE_DECLARE(programming, CONFIG_APP_LOG_LEVEL);

// ── Types ─────────────────────────────────────────────────────────────────────
enum class Event { Tick = 0, Shutdown = 1 };

using Callback = std::function<void(Event)>;

// ── Simple event dispatcher ──────────────────────────────────────────────────────────
class EventDispatcher {
 public:
  void add(Callback cb) { _callbacks.push_back(std::move(cb)); }

  void dispatch(Event e) {
    for (auto& cb : _callbacks) {
      cb(e);
    }
  }

 private:
  std::vector<Callback> _callbacks;
};

// ── Shared resource ───────────────────────────────────────────────────────────
struct SharedLog {
  std::vector<std::string> entries;
  std::string name;

  explicit SharedLog(std::string n) : name(std::move(n)) {}

  void append(const std::string& msg) {
    entries.push_back(msg);
    LOG_INF("[%s] logged: '%s'", name.c_str(), msg.c_str());
  }

  ~SharedLog() {
    LOG_INF("SharedLog [%s] destroyed - %zu entries", name.c_str(), entries.size());
  }
};

// ── 1. Dangling reference — what NOT to do ────────────────────────────────────
[[maybe_unused]] static Callback make_dangling_callback() {
  int local_counter = 100;

  // Captures local_counter by reference.
  // local_counter is destroyed when this function returns.
  // Calling the returned lambda is undefined behaviour.
  return [&local_counter](Event) {
    // local_counter is a dangling reference here
    LOG_INF("[value reference] counter = %d",
            local_counter);  // Undefined behavior — do not do this
  };
}

// ── 2. Capture by value — safe for locals ────────────────────────────────────
static Callback make_safe_callback_by_value() {
  int local_counter = 100;

  // Captures a copy of local_counter.
  // The lambda owns its copy — no dangling reference possible.
  return [local_counter](Event e) mutable {
    if (e == Event::Tick) {
      local_counter++;
      LOG_INF("[value capture] counter = %d", local_counter);
    }
  };
}

// ── 3. shared_ptr capture — shared ownership ─────────────────────────────────
static Callback make_shared_ownership_callback(std::shared_ptr<SharedLog> log) {
  // Lambda captures shared_ptr by value — extends lifetime of the log
  // for as long as the lambda exists.
  return [log](Event e) {
    if (e == Event::Tick) {
      log->append("tick received");
    } else if (e == Event::Shutdown) {
      log->append("shutdown received");
    }
  };
}

// ── 4. weak_ptr capture — observe without extending lifetime ──────────────────
static Callback make_weak_callback(std::weak_ptr<SharedLog> weak_log) {
  // Captures weak_ptr — does NOT extend the lifetime of SharedLog.
  // The callback checks whether the object still exists before using it.
  // This is the correct pattern when the callback should not prevent
  // the owner from destroying the resource.
  return [weak_log](Event e) {
    auto log = weak_log.lock();  // try to acquire strong reference
    if (!log) {
      LOG_INF("[weak capture] SharedLog already destroyed - skipping");
      return;
    }
    if (e == Event::Tick) {
      log->append("tick (weak)");
    }
  };
}

void callback_lifetime() {
  EventDispatcher dispatcher;

  // ── Dangling reference (commented out — undefined behavior if called)
  // ─────────────────────
  //
  LOG_INF("── Capture by reference ──");
  Callback dangling_cb = make_dangling_callback();
  dispatcher.add(dangling_cb);  // DO NOT do this — calling it is undefined behavior
  dispatcher.dispatch(Event::Tick);

  // ── Capture by value — safe ───────────────────────────────────────────────
  LOG_INF("── Capture by value ──");
  dispatcher.add(make_safe_callback_by_value());
  dispatcher.dispatch(Event::Tick);

  // ── shared_ptr capture ────────────────────────────────────────────────────
  LOG_INF("── shared_ptr capture ──");
  {
    auto shared_log = std::make_shared<SharedLog>("shared capture");

    // Two callbacks share ownership of the same log
    dispatcher.add(make_shared_ownership_callback(shared_log));
    dispatcher.add(make_shared_ownership_callback(shared_log));

    LOG_INF("shared_log use_count before publish: %ld",
            shared_log.use_count());  // local + 2 lambdas

    dispatcher.dispatch(Event::Tick);

    LOG_INF("shared_log use_count after publish: %ld",
            shared_log.use_count());  // identical: local + 2 lambdas

    // shared_log goes out of scope here but the lambdas in dispatcher still
    // hold shared_ptr copies — SharedLog is NOT destroyed yet
  }
  LOG_INF("shared_log local reference gone - lambdas still hold it alive");

  // ── weak_ptr capture ──────────────────────────────────────────────────────
  LOG_INF("── weak_ptr capture ──");
  {
    auto observable_log = std::make_shared<SharedLog>("observable");
    dispatcher.add(make_weak_callback(observable_log));

    LOG_INF("observable_log alive - dispatching tick:");
    dispatcher.dispatch(Event::Tick);

    // Destroy observable_log — weak_ptr callback should detect this
    LOG_INF("Destroying observable_log...");
    observable_log.reset();  // reference count → 0, object destroyed

    LOG_INF("observable_log destroyed - dispatching tick:");
    dispatcher.dispatch(Event::Tick);  // weak_ptr callback gracefully skips
  }

  // ── Shutdown event ────────────────────────────────────────────────────────
  LOG_INF("── Dispatching shutdown ──");
  dispatcher.dispatch(Event::Shutdown);
}
src/callback_lifetime.hpp
src/callback_lifetime.hpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file callback_lifetime.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief Demonstrates lifetime and capture discipline.
 *
 *   - Dangling reference from capturing a local by reference
 *   - Capture by value as the safe alternative
 *   - shared_ptr capture for shared ownership across callbacks
 *   - Weak_ptr capture to avoid extending lifetime unintentionally
 *
 *
 *
 * @date 2025-07-01
 * @version 1.0.0
 ***************************************************************************/

#pragma once

void callback_lifetime();