Skip to content

Instantly share code, notes, and snippets.

@asd142513
Last active June 10, 2025 05:19
Show Gist options
  • Save asd142513/ee494971fc93b46a4ed81935e2e4261a to your computer and use it in GitHub Desktop.
Save asd142513/ee494971fc93b46a4ed81935e2e4261a to your computer and use it in GitHub Desktop.
/*
Original post:
https://thephd.dev/c2y-the-defer-technical-specification-its-time-go-go-go
Inspired by for loop and Python’s `with` statement:
https://en.cppreference.com/w/c/language/for.html
for ( init-clause ; cond-expression ; iteration-expression ) loop-statement
https://docs.python.org/3/reference/compound_stmts.html#with
Syntax:
with ( init-clause ; cond-expression ; cleanup-expression ) context-statement
Comparison: `thephd.dev defer` vs `with`
1. Automatically creates a new scope.
2. Supports only expressions — no deferred statements.
3. `break` is allowed within the `with` block.
4. Nested `with` statements are not allowed within init-clause, cond-expression, or cleanup-expression.
However, the context-statement can include `with`, just like in loop bodies.
5. No mid-scope defers.
- In the original `defer`, deferred statements could appear anywhere in the scope.
- This is suboptimal, as developers must read the entire block to find all deferred actions.
- The `with` statement improves clarity by forcing all deferred behavior to be declared at the start of the block.
6. Unlike `defer`, which helps maintain a flat block structure `with` introduces an additional level of nesting.
This is even worse in common `malloc`–`init_object` patterns as it introduces two levels of additional nesting.
Comparison: `for` loop vs `with` block
1. Trivially, context-statement is executed only once; `continue` is not allowed.
2. Unlike a `for` loop's iteration-expression, the cleanup-expression is evaluated when exiting via `break` or `return`.
Jump to outside of block with `goto` skips the cleanup-expression.
*/
#include <threads.h>
extern int do_sync_work(int id, mtx_t *m);
int main(void)
{
with (mtx_t m = {}; mtx_init(&m, mtx_plain) == thrd_success; mtx_destroy(&m)) {
for (int i = 0; i < 12; ++i) {
with (; mtx_lock(&m) == thrd_success; mtx_unlock(&m)) {
if (do_sync_work(i, &m) == 0) {
return 1;
}
}
}
}
return 0;
}
@fdwr
Copy link

fdwr commented Jun 10, 2025

Arriving here from: ThePhD/future_cxx#67 (comment)

I've written up an alternative approach to the proposed defer-statement. Any comments would be appreciated!

🤔 defer feels more foundational than with, where with is a compound aggregation of {initialization statement, if, defer}, and so one could easily compose with if defer existed (even a preprocessor macro could implement with given defer), but one could not implement defer via with, since defer supports block cleanup rather than only expressions.

Interestingly (for an analog), the for loop is a combo of {initialization statement, an if, increment expression}, and thus an if is more foundational of a control flow primitive than the for. Although one could actually implement an if statement in terms of a for loop (e.g. for (bool b = expression; b; b = false) { ... }), it feels quite upside-down. 🙃

I'm not opposed to a Pythonesque with eventually (similar to C#'s using, and not to be confused with VB/Pascal-style with which is very different), especially if it could behave like Python's concise with where an object's enter/exit methods were called (kinda like how in C++ begin/end methods are specially recognized for ranged for loops), but I'd vote for an order of operations where we introduce the more general/fundamental construct first.

For now, the defer approach isn`t much longer:

mtx_t m = {};
if (mtx_init(&m, mtx_plain) == thrd_success)
{
	defer mtx_destroy(&m);
	for (int i = 0; i < 12; ++i) {
		if (mtx_lock(&m) == thrd_success) {
			defer mtx_unlock(&m);
			if (do_sync_work(i, &m) == 0) {
				return 1;
			}
		}
	}
}

@asd142513
Copy link
Author

I strongly agree that defer is more foundational than with. But that’s precisely the point. defer has too high degree of freedom. While restricting programmer freedom might go against the philosophy of the C language, it’s still important to find a sweet spot — just as we have for and while loops despite the existence of goto. My concern is that defer might end up being confined to strictly limited use cases, much like goto.

Additionally, defer introduces a gap between initialization and cleanup. Carelessly inserted code might interfere with proper handling of the defer statement. In contrast, with helps eliminate this gap.

One critical feature that the with statement lacks, compared to defer, is handling initialization failure. While a with-else could be introduced to handle failures in the condition expression, the lack of a variable to store the evaluated value of the condition expression could pose a serious problem. This issue is shared with for loops, but it's more critical in this context because we are performing initialization rather than just checking a condition.

The with statement isn't perfect either, but I believe it's important to constrain the excessive degree of freedom that defer introduces.

@asd142513
Copy link
Author

One critical feature that the with statement lacks, compared to defer, is handling initialization failure. While a with-else could be introduced to handle failures in the condition expression, the lack of a variable to store the evaluated value of the condition expression could pose a serious problem. This issue is shared with for loops, but it's more critical in this context because we are performing initialization rather than just checking a condition.

Possible solution:

mtx_t m = {};
with (int ret = mtx_init(&m, mtx_plain); ret == thrd_success; mtx_destroy(&m)) {
	for (int i = 0; i < 12; ++i) {
		with (; mtx_lock(&m) == thrd_success; mtx_unlock(&m)) {
			if (do_sync_work(i, &m) == 0) {
				return 1;
			}
		}
	}
} else {
	if (ret == thrd_error) {
		// ...
	}
}

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