If you write traditional C like:
BINDING(cellA) = BINDING(cellB);
Regardless of what the BINDING() macro expands to, there's no way to do any sort of validation in any build (debug or otherwise). Whatever this becomes can only be a simple assignment.
But if you instead wrote:
Tweak_Binding(cellA, Binding_Of(cellB));
Then even if the intent is just a simple assignment in a release build, you could define that in a checked build to something that runs arbitrary code :
#if NO_RUNTIME_CHECKS
#define Tweak_Binding(dest,src) \
(dest)->extra.binding = (src)->extra.binding
#else
#define Tweak_Binding(dest,src) \
Tweak_Binding_With_Added_Checks((dest), (src))
#endif
But if you're willing to confine your extra checks to C++ builds, you can leave the source looking like BINDING(cellA) = BINDING(cellB)
This is because C++ has operator overloading, and you can create a wrapper class which "does the right thing", e.g. notices whether a BINDING(...)
is used as a left hand side operator or right side operator.
Just Because I Can, Does That Mean I Should?
Most of my original rationale was that I didn't want the source to make it look like you were making a function call when you were not.
When you see something like Tweak_Binding(...)
you don't know how many instructions that's going to be, or what side effects its going to have. But when you see a plain assignment (that you know compiles in C to something quite simple), then that tells you not much is going on.
However...I specifically came up with the term "Tweak"
as a convention for functions like this that do very little. It's a pretty good name, and it's learnable to say "oh, if it says Tweak, that means it's as cheap as an assignment".
Here are some of the points to consider:
-
Because things like BINDING() are doing weird magic allowing them to be on the left side of assignments, by convention I believe they must be in all caps. This is kind of noisy.
- On the plus side, it can be brief, with the same term used for extracting and for tweaking. Since datatypes use the leading-caps with no underscore convention, plain
Binding()
looks like a parameterized datatype, so it would have to beBinding_Of()
for the extractor
- On the plus side, it can be brief, with the same term used for extracting and for tweaking. Since datatypes use the leading-caps with no underscore convention, plain
-
Using C++ magic to get the RUNTIME_CHECKS means the C build won't have assertion-parity with the C build. Bugs that only happen in the C build (perhaps on platforms that only offer C) would thus be harder to find.
-
The C++ operator overloading is going to be over the heads of those reading the code who only know C.
-
In debug builds, the C++ compilers do not inline the operator overloading, constructors, etc. that are involved in making the weird objects that are behind the scenes making the trick work. That means using the C++ debugger to step into an expression like
BINDING(cellA) = BINDING(cellB)
takes an annoying number of step-in and step-out operations.
Here's The "Scary" Implementation
It really isn't rocket science, but it is something. And it's something that can't be compiled by plain C compilers, meaning you can't do DEBUG_CHECK_BINDING in C builds.
#if (! DEBUG_CHECK_BINDING)
#define BINDING(cell) \
*x_cast(Context**, m_cast(Node**, &(cell)->extra.node))
#else
struct BindingHolder {
Cell* & ref;
BindingHolder(const Cell* const& ref)
: ref (const_cast<Cell* &>(ref))
{
assert(Is_Bindable_Heart(Cell_Heart(ref)));
}
void operator=(Stub* right) {
Assert_Cell_Writable(ref);
ref->extra.node = right;
Assert_Cell_Binding_Valid(ref);
}
void operator=(BindingHolder const& right) {
Assert_Cell_Writable(ref);
ref->extra.node = right.ref->extra.node;
Assert_Cell_Binding_Valid(ref);
}
void operator=(nullptr_t) {
Assert_Cell_Writable(ref);
ref->extra.node = nullptr;
}
template<typename T>
void operator=(Option(T) right) {
Assert_Cell_Writable(ref);
ref->extra.node = maybe right;
Assert_Cell_Binding_Valid(ref);
}
Context* operator-> () const
{ return x_cast(Context*, ref->extra.node); }
operator Context* () const
{ return x_cast(Context*, ref->extra.node); }
};
#define BINDING(cell) \
BindingHolder{cell}
#endif
Looking at them Side-By-Side
BINDING(cellA) = BINDING(cellB);
Tweak_Binding(cellA, Binding_Of(cellB));
I do feel a pretty strong bias for the briefer notation...
But I can see the argument for not doing the "weird" thing when there's no increase in functionality... in fact, only losing functionality in C builds. (Most C++-isms are there to add something that would be fundamentally not possible in C, vs. just syntax sugar like this.)
The invention of the "Tweak" term does change my calculation a little bit here. Because if it's used only in these "single-assignment-equivalent" circumstances, you can comprehend it as a C assignment statement.