Anyways, with statements.
I like with statements. They make things look so nice - your scope doesn't get cluttered up with useless variables or variables you only need once. They're lovely. Take, as an example, the following Common Lisp:
; look, ma, no cluttered namespace!
(let ((x 3) (y 4))
(* x y)) ; => 12
Which is neat.
And there are serious benefits to this. It's common knowledge in programming circles that you can only have on average 7 variables in your head with ease (at least, according to Documentation/CodingStyle in the Linux kernel source tree). Using with statements allows you to avoid the problem of keeping track of 15 variables all declared and initialized at the beginning of a function, something I have seen all too frequently in production C (though, to be fair, it's going away now; check out block/blk-core.c in the Linux kernel source tree for an excellent example of declaring many variables at the beginning of a function).
Long story short, we should get in the habit of declaring variables only when we need them and use them for only as long as we have to to keep our heads clear
of useless junk. One way we can solve this is with with statements.
But how do we implement a with statement in C?
Simple. We look to our old friend, the C99 standard.
6.8.5.3 The for statement
for ( clause-1 ; expression-2 ; expression-3 )
behaves as follows: The expression expression-2 is the controlling expression that is evaluated before each execution of the loop body. The expression expression-3 is evaluated as a void expression after each execution of the loop body. If clause-1 is a declaration, the scope of any variables it declares is the remainder of the declaration and the entire loop, including the other two expressions; it is reached in the order of execution before the first evaluation of the controlling expression. If clause-1 is an expression, it is evaluated as a void expression before the first evaluation of the controlling expression.)
Oh, neat. So we can make arbitrary lexical scopes with for loops. Not only that, but we can force our condition to be true or false whenever we want it to be, because the lexical scope of the variables also includes the other statements in the for loop.
But how do we break out of them?
Simple - we just need another variable.
Now that we have this down, let's write a macro.
Before we get into this, there's a caveat - although you can declare multiple variables, you can't have multiple declarations. Everything in the with statement has to be of the same type. This isn't actually too much of a problem, because the control structure is nestable, so if you need variables of different types, stack the withs on top of each other.
Before we get into this, there's a caveat - although you can declare multiple variables, you can't have multiple declarations. Everything in the with statement has to be of the same type. This isn't actually too much of a problem, because the control structure is nestable, so if you need variables of different types, stack the withs on top of each other.
#define with(...) for (__VA_ARGS__, *_wth=(void*)1; _wth; _wth = NULL)
Here, it's what you'd expect. On top of the declarations passed by the user, we define our own statement which we know is going to evaluate to true at first, then false on the second run-through. Note that this is nestable - we only care about the value of _wth in the clauses of the for-statement; if we have a for-statement which defines the same variable in clause 1, the value will supersede the _wth in the outer for, but won't change its value or rebind it. This is perfectly safe.
We also use a variadic macro because we don't know how many assignments are being passed. Pretty straightforward.
The last thing that's important in understanding this macro is why _wth is declared the way it is. The thing is, the way this'll expand is as follows:
with (int i=3,j=4)
=> for (int i=3,j=4,*wth=(void*)1; /* ... */)
So _wth is actually just a pointer to whatever type the temporary variables are. We need to use a pointer because we're not guaranteed to get an integral type - we could just as easily get a struct or a union as an int or a pointer. This is a consistent and type-safe way of doing it, and it makes sure it always works.
So, now we have a very nice with statement. Now, we can write
with (FILE *stream = fopen("/etc/passwd", "r")
printf("%s\n", file_to_string(stream));
A multiple-statement flavor looks like this:
with (FILE *stream = fopen("/etc/passwd", "r") {
do_stuff_with_stream(stream);
printf("%s\n", file_to_string(stream));
}
We can also bind multiple temporary variables like so:
with (int i = 2, j = 3)
printf("i=%d\nj=%d\n", i, j);
And, of course, we can nest with statements, if you're using a variety of types:
with (int i = 2)
with (float j = 3.0f)
printf("i=%d\nj=%f\n", i, j);
I recommend, if multiple types are needed, stacking the withs on top of each other like this (this can be done just like this after if-statements without brackets, but it may be more readable to put them anyways):
with (int j = 3)
with (float d = 4.0f)
with (struct foo bar = baz()) {
/* do things with j, d, and bar */
}
Of course, you could also put them all on the same line.
I should add that this is functionally the same as just making a new block, e.g.
{
struct foo *bar = baz();
use_bar(bar);
destroy_foo(bar);
}
The reason I like with versus this is because firstly, this syntax is a lot more consistent with normal C control structures, a la if and while. Also, using with emphasizes the variables and separates the variables from the code, allowing the reader to more quickly understand the purpose of the block. For example, it takes a lot less time to understand
with (FILE *file = fopen(...))
...
than it does to understand
{
FILE *file = fopen(...);
...
}
So, now we have a nice syntax. Cool.