Dynamic Scoping in C++
Date: 2020-07-10
I always had a faible for dynamic scoping as it is implemented in Common Lisp, Emacs Lisp, or LaTeX. To me, it seems that dynamic scoping is an almost forgotten technique and dismissed technique in modern programming. While it is more complex to understand and more magically than lexical scoping, there are some applications that benefit from dynamic scoping.
In lexical scoping, if you access a variable, your compiler will start
searching the declaration from the point of reference outwards in
lexical order. This means it searches first in the most inner scope,
and continoues to widen its search scope by the "closed-nested-scope
rule". For example, in the following code snippet, the call to foo()
would return 23, as the lexcially-closest declaration is the global variable.
int config = 23; return foo() { return config; } void bar() { int config = 42; printf("%d\", foo()); }
With dynamic scoping, the world would look different as the closest
(time-wise) dynamic binding for a given name is found instead of
lexically closest. In the example, foo()
would return 42, as the
binding in bar()
happened more recently that the global binding.
You can think of a dynamically-scoped global variable as shadowable global binding. By creating a new binding for the variable, we shadow the old binding, and all references in child functions will now refer to the new binding, although they have not received the value via an argument transfer. Ergo, dynamically-scoped variables are shadowable side-channels that can influence the behavior of a function.
For example, in Common Lisp, the variable *standard-output*
is
dynamically scoped and functions (print)
simply send there
characters to that variable. As it is dynamically scoped, we can just
redirect all output to a file by creating a new binding for
*standard-output*
.
(with-open-file (*standard-output* "somefile.dat" :direction :output :if-exists :supersede) (print "this goes into file"))
Wouldn't it be neat to do the same in, let's say, C++? Luckily, with templates, the RAII pattern, and some template magic, we can write code like this:
DynamicScope<int> G(0); void bar() { std::cout << "bar " << *G << std::endl; } void foo() { DynamicScope<int>::BindInstance _(G, 3); bar(); } int main() { std::cout << "main1 " << *G << std::endl; foo(); std::cout << "main2 " << *G << std::endl; }
and end up with the output:
main1 0 bar 3 main2 0
The source for the DynamicScope<T>
can be found here: dynamic_scope.cc
[UPDATE 23.09.2020] Multi-Threaded Programs
After some discussion on HackerNews (which I highly recommend to read), I was made aware that the previous implementation has a problem with multi-threaded programs. As all threads share the same global objects as a root to their dynamic-scope value stack, bindings in one thread had an influence on the bindings in another thread. Of coure, this would be a broken version of dynamic scoping.
The problem can be circumvented by making the global variable thread local:
thread_local DynamicScope<int> G(0);
With this, every thread has its own version of the G object, which internally is the head of a linked list of the shadowed values. However, someone could forget the add the required thread_local attribute.
Unluckily, checking whether the storage class of a given variable is
thread_local is not that trivial. However, I came up with a rather
low-cost sanity check that ensures that a variable must be declared
thread_local
. The updated version of DynamicScope<T>
, as well as
some benchmarking code, can be found here: thread_dynamic_scope.cc