Skip to content

add Go-style defer mechanism for C++#249

Open
MatzeOGH wants to merge 2 commits into
AlpineMapsOrg:mainfrom
MatzeOGH:main
Open

add Go-style defer mechanism for C++#249
MatzeOGH wants to merge 2 commits into
AlpineMapsOrg:mainfrom
MatzeOGH:main

Conversation

@MatzeOGH

@MatzeOGH MatzeOGH commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Introduce a lightweight defer macro that executes a lambda when leaving
scope, providing a simple way to express cleanup actions in C++.
The implementation uses RAII and relies on compiler inlining to generate code
close to hand-written cleanup paths.

Code generation comparison:
https://godbolt.org/z/PWzjY66Ga

Example:

FILE* f = fopen("file.txt", "r");
if (!f) return;

defer { fclose(f); };

The deferred action runs automatically at the end of the scope, including
when leaving through early returns.

@adam-ce

adam-ce commented Jul 8, 2026

Copy link
Copy Markdown
Member

thanks for the pull request!
Though I have to say that I consider the change dangerous. macros in general are a code smell for me. both examples you gave (here and godbolt) should be solved in a different way (using Qt or similar IO instead of c-style for this example; using objects on the stack in the godbolt example).

that being said, I recognise that a functionality like this might be necessary, so please give a better example to convince me there is no better way.

even if we have to accept a macro, the name is no good. macros should scream that they are bad (ALL_CAPS and a long inconvenient name, so it doesn't proliferate), and the name mustn't be at risk of being used anywhere else. finally, macros are unscoped, so we also have to prevent conflicts with other libs. so this would mean a name like ALP_DEFER_UNTIL_END_OF_SCOPE).

@MatzeOGH

MatzeOGH commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

I agree with most of your concerns about macros. I don't see macros as an ideal language feature either, and if C++ had a built-in defer or if std::scope_exit were universally available and ergonomic enough for our use case, I wouldn't propose a macro at all. The macro is only there to work around a missing language feature.

I also agree that the examples I provided weren't the strongest. In particular, the FILE* example is easily dismissed because modern C++ offers better abstractions.

The use case I'm trying to address is not resource ownership where RAII is clearly the right solution, but scope-exit actions that are local to a single scope. Typical examples are balancing API calls (begin.../end..., push.../pop...), temporarily modifying state that must be restored, or accumulating several independent cleanup actions while using multiple early returns.

For example, in a WebGPU render function:

bool render_shadow_map(Scene& scene)
{
    auto encoder = device.createCommandEncoder();

    auto pass = encoder.beginRenderPass(shadowDesc);

    defer {
        pass.end();
    };

    encoder.pushDebugGroup("Shadow Pass");
    defer {
        encoder.popDebugGroup();
    };

    if (!compile_pipeline())
        return false;

    if (!upload_light_data())
        return false;

    for (auto& mesh : scene.meshes) {

        if (!mesh.visible())
            continue;

        if (!mesh.upload())
            return false;

        if (!mesh.bind(pass))
            return false;

        if (!mesh.draw(pass))
            return false;
    }

    auto commands = encoder.finish();

    if (!commands)
        return false;

    queue.submit(commands);

    return true;
}

Here the cleanup isn't about owning pass or encoder. Tt's about satisfying API protocol regardless of which of several exit paths is taken. The alternatives are either duplicating cleanup before every return, restructuring the function around a single exit, or introducing one-off RAII wrapper classes whose only purpose is to call end() or popDebugGroup() in their destructor.

I think this is the niche where defer is valuable: it complements RAII rather than replacing it. If the cleanup naturally belongs to a reusable type, RAII is still the better solution. If the cleanup is algorithm-specific and only meaningful within a single function, an inline scope-exit action is often the simpler expression.

Regarding the macro name, I understand the motivation for making macros look "ugly" to discourage their use. However, in this case the macro isn't introducing a project-specific abstraction—it is emulating a language construct that already exists in several languages (Go, Zig, Jai, Odin, etc.).

The intention is that when someone sees

defer { ... };

they immediately recognize the semantics from those languages: execute this block when leaving the current scope. Giving it a long, project-specific name like ALP_DEFER_UNTIL_END_OF_SCOPE doesn't make the feature any safer or less powerful. Tt just obscures the fact that it's the familiar defer construct.

In other words, the macro isn't trying to make scope-exit actions more ergonomic than existing C++ facilities. It's trying to express an already well-known concept using the spelling people expect. The implementation happens to require a macro because C++ lacks native syntax, but ideally users should think of it as "the missing defer keyword," not as "yet another project macro."

I agree with moving the helper macros to the ALP_DEFER_... naming scheme, as they are implementation details and should be clearly namespaced to avoid collisions.

@adam-ce

adam-ce commented Jul 9, 2026

Copy link
Copy Markdown
Member

hi,
thanks for the thorough answer. the new example makes much more sense :)

as to the name, we could debate that for some time, but i'd like to cut it short:

  • i agree that your syntax looks quite nice.
  • unfortunately it's not flying, because it's a macro, and we don't want to break usecases like bool defer = true. And since it's a macro, i would highlight that fact by making it ALL_CAPS (make it safer by the highlight), and the ALP_ prefix is to prevent other clashes (like we had with Catch2). ALP_DEFER would be fine, until end of scope would have made it clearer for me, but also ok with short.

you mentioned scope_exit, which also looks like a nice syntax to me. can't we just include offa/scope-guard? we did something similar with tl::expected here. What do you think about that?

@MatzeOGH

MatzeOGH commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Hi,
If the macro is the only thing blocking integration, I can change it to a scope_exit struct instead. The syntax would change from:

defer { ... };

to:

nucleus::scope_exit guard([&] { ... });

This has the nice advantage that the implementation can later be swapped to the C++23 version without requiring any code changes. Only the namespace would need to change from nucleus to std and the implementation can be removed.

I personally would prefer not to add a dependency just for this kind of small convenience feature. The implementation is simple enough to keep local, and avoiding an additional dependency keeps the project easier to maintain.

@adam-ce

adam-ce commented Jul 9, 2026

Copy link
Copy Markdown
Member

yes, i like the scope_exit guard version much more. it's a bit more code, but it feels much more idiomatic.

about including it via dependency or our own implementation: imo the pros and cons pretty much balance each other out. the library is certainly better tested, and people thought about failure cases and a optimal implementation. and there is unique_resource as well, but that's as easy to implement on our own. I do see your point about having an unnecessary dependency, it's still a git clone and cmake noise. + we don't have exceptions, which likely is the most problematic part. so yeah, either way works, really :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants