January 23rd, 2026
0 reactions

C++ has scope_exit for running code at scope exit. C# says “We have scope_exit at home.”

The Windows Implementation Library (commonly known as wil) provides a helper called scope_exit which creates and returns an RAII object whose destructor runs the lambda you specify. This helper provides roughly equivalent functionality in C++ to what is called tryfinally in other languages such as C#.

// If there is an active primary Gadget, then do a bunch of stuff,
// but no matter what, make sure it is no longer active when finished.
var gadget = widget.GetActiveGadget(Connection.Primary);
if (gadget != null) {
    try {
        ⟦ lots of code ⟧
    } finally {
        widget.SetActiveGadget(Connection.Primary, null);
    }
}

One thing that is cumbersome about this pattern is that the cleanup code is far away from the place where the cleanup obligation was created, making it harder to confirm during code review that the proper cleanup did occur. Furthermore, you could imagine that somebody makes a change to the GetActiveGadget() call that requires a matching change to the SetActiveGadget(), but since the SetActiveGadget() is so far away, you may not realize that you need to make a matching change 200 lines later.

var gadget = widget.GetActiveGadget(Connection.Secondary);
if (gadget != null) {
    try {
        ⟦ lots of code ⟧
    } finally {
        widget.SetActiveGadget(Connection.Secondary, null);
    }
}

Another thing that is cumbersome about this pattern is that you may create multiple obligations at different points in the code execution, resulting in deep nesting.

var gadget = widget.GetActiveGadget(Connection.Secondary);
if (gadget != null) {
    try {
        ⟦ lots of code ⟧
        if (gadget.IsEnabled()) {
            try {
                ⟦ lots more code ⟧
            } finally {
                gadget.Disable();
            }
        }
    } finally {
        widget.SetActiveGadget(Connection.Secondary, null);
    }
}

Can we get scope_exit ergonomics in C#?

You can do it with the using statement introduced in .NET 8 and a custom class that we may as well call Scope­Exit.

public ref struct ScopeExit
{
    public ScopeExit(Action action)
    {
        this.action = action;
    }

    public void Dispose()
    {
        action.Invoke();
    }

    Action action;
}

Now you can write

var gadget = widget.GetActiveGadget();
if (gadget != null) {
    using var clearActiveGadget = new ScopeExit(() => widget.SetActiveGadget(null));
    ⟦ lots of code ⟧
    if (gadget.IsEnabled()) {
        using var disableGadget = new ScopeExit(() => gadget.Disable());
        ⟦ lots more code ⟧
    }
}

, Although many objects implement IDisposable so that you can clean them up with a using statement, it’s not practical to have a separate method for every possible ad-hoc cleanup that could be needed, and the ScopeExit lets us create bespoke cleanup on demand.¹

I’m adding this to my list of “Crazy C# ideas which never go anywhere,” joining CastTo<T> and As<T> and ThreadSwitcher.

¹ There might be common patterns like “If you Open(), then you probably want to Close()” which could benefit from a disposable-returning method, but you still have to wrap the result.

public ref struct OpenResult
{
    public bool Success { get; init; }

    public OpenResult(Widget widget)
    {
        this.widget = widget;
        Success = widget.Open();
    }

    public void Dispose()
    {
        if (Success) {
            widget.Close();
        }
    }

    private Widget widget;
}
Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

5 comments

Sort by :
  • Otul Osan 3 hours ago · Edited

    On Windows 9x, what was the use cases for C:\WINDOWS\WINSTART.BAT compared to C:\AUTOEXEC.BAT, and at which phase of the startup process it's launched?

    Could we see an explanation of the whole Windows 9x startup and shutdown process one day? On Wikipedia, The 16-bit modules are loaded doesn't tell what precisely technically happens after WIN.COM is launched, besides loading modules.
    For example I don't know what module switches to VGA mode in the boot process, draws the busy cursor on black background for 2 seconds and then paints the wallpaper.

    Read more
    • Dan Bugglin 3 hours ago

      AUTOEXEC.BAT was executed by MS-DOS as part of the boot process (the second to last step before launching COMMAND.COM I think). Win 9x shipped on top of MS-DOS 7.x so that meant you got AUTOEXEC.BAT and the full MS-DOS trimmings. Windows NT was built from the ground up to not need MS-DOS so no MS-DOS stuff.

      I checked Windows 98 and Windows 3.1 VMs I have, neither have a WINSTART.BAT, and I have not heard of it before despite using Windows since 3.1. Back in those days you just ran WIN.COM to launch Windows, though sometimes you wanted to switch out...

      Read more
      • Otul Osan 3 hours ago · Edited

        If you want to try (Windows 95), WINSTART.BAT is executed at a later stage in the boot process in protected mode, create a file in C:\WINDOWS\WINSTART.BAT and put COMMAND.COM as the content.

  • George Styles 5 hours ago

    Ive used this type of approach before when writing my own profiler. Start the method with

    using (new MyProfiler("LoadData")) 

    records the tick time in the constructor, and it automatically calls the dispose method when scope is exitted, at which point we calculate how many ticks and record.

  • GL 5 hours ago · Edited

    It rightly is a crazy idea that should never go anywhere, because that type doesn't give the similar level of guarantee as WIL scope_exit.

    Allocation of the delegate could fail at the lambda expression. This creates a window where exception can be thrown (I'm ignoring asynchronous thread aborts, which is either impossible in new .NET or can be guarded by CER in .NET Framework) but the scope-exit action is not set up. For non-capturing lambdas, under current optimization, the failure could happen the first time that line is executed. For capturing lambdas, it introduces an allocation each time that line is...

    Read more