How to Construct a Bit Mask in VBA
What's the safest way to construct a bit mask in VBA? Your preferred approach may be more dangerous than you realize.
tl;dr: Use OR
not +
Bit masks are a useful way to pass multiple boolean values to a routine, especially when those values are mostly false. An example most VBA developers are familiar with is the buttons argument of the MsgBox function:
Numeric expression that is the sum of values specifying the number and type of buttons to display, the icon style to use, the identity of the default button, and the modality of the message box. If omitted, the default value for buttons is 0.
Magic Numbers: The Worst Approach
You can combine the MsgBox constants to control the display of the message box dialog window.
Let's say you wanted to show a warning with Yes and No buttons where the second button is the default. You would combine the following three values:
- 4: Yes and No buttons
- 48: Warning message
- 256: Second button is default
If you add up the three numbers above–4 + 48 + 256
–you get 308
. So, you could pass the number 308 as the buttons argument:
Of course, this would be terrible programming practice. The literal 308
is what's known as a "magic number" in programming.
Addition of Constants: A Big Improvement
Instead of passing 308
in literal form to the function, it's better to build up the number from its component MsgBox constants:
- 4: vbYesNo
- 48: vbExclamation
- 256: vbDefaultButton2
So, a better way to call the same message box is to add the constants as shown in the caption below:
This is better than using magic numbers, but there's still room for improvement.
The problem is that we are using addition to combine the constants. When constructing a bit mask, it is always safer to use a bitwise OR operation.
The problem with using addition to build a bit mask
Let's say that we have the following convenience function:
'Returns True if user presses [Yes] button, False otherwise
Function UserConfirms(Options As VbMsgBoxStyle) As Boolean
UserConfirms = (MsgBox("Are you sure?", vbYesNo + Options) = vbYes)
End Function
The intent of the function is to always show Yes and No buttons, but to allow the calling code to change the default button or the displayed icon (e.g., question mark, exclamation point, etc.).
We can call this function as shown in the caption below and it works just fine. The [Yes] and [No] buttons are shown, the second button is the default, and the icon is the Exclamation point.
But what happens if, when writing the calling code, we forget that the vbYesNo option is included within the UserConfirms function? Then, "just to be on the safe side", we also include the vbYesNo option when calling the code. Here's what happens:
What happened? The vbYesNo constant got added in twice. The calling code passed in vbExclamation (48) + vbDefaultButton2 (256) + vbYesNo (4) for a total of 308. The UserConfirms function then took the 308 and added it to the vbYesNo (4) constant for a total of 312.
Bitwise OR Operations: A Safe Way to Build Bit Masks
We can avoid the problem above if we replace the addition operation inside the UserConfirms function with a bitwise OR operation:
It does not matter if we include the vbYesNo constant when we call the function now. The bitwise OR operation prevents it from being "double-counted."
Visualizing the Bit Mask
The above explanation is difficult to follow if you are unfamiliar with the concept of bit masks.
Hopefully, the graphic below will clarify what's going on a bit (pun intended).
The top part of the graphic simply maps the MsgBox constants to their corresponding values.
The first row of numbers represent the binary values of their respective columns. This is analogous to decimal notation where the first digit represents the "ones" column, the second digit represents the "tens" column, the third digit represents the "hundreds" column, etc. Except, in the case of binary, the first digit represents the "ones" column, the second digit represents the "twos" column, the third digit represents the "fours" column, etc.
The bottom half of the graphic shows the two calculations: first, the basic addition and second, the bitwise OR operation.
In the basic addition section, we add 4 plus 308 and get 312. More importantly, though, the value in the "fours" column becomes 0 as the number in the "eights" column becomes 1. The binary math is just like decimal math: one (1) plus one (1) equals two (10), so we put zero in the "fours" column and carry the one over to the "eights" column. (This is the equivalent of "five (5) plus five (5) equals ten (10), put zero in the column and carry the one.") The key numbers are highlighted in red.
If instead, we perform a bitwise OR operation, 4 OR 308 equals 308. That's because of how the two 1's are handled in the "fours" column. You never "carry" numbers when doing a bitwise OR. If either or both numbers are 1, the OR'ed result is 1. The OR'ed result is only 0 if both numbers are 0. The numbers highlighted in green demonstrate this concept.
Revisiting the Examples
The message box buttons are determined by the value of the first three binary digits; the "ones", "twos", and "fours" columns combine to create values between 0 (vbOKOnly) and 5 (vbRetryCancel).
In the addition example, the result of adding 4 + 4 is that these first three columns are all set to 0 (refer to the 312 row). Since a value of 0 corresponds with vbOKOnly, we see the [OK] button only in that example:
In the bitwise OR example, though, the result of OR'ing 4 with 4 is 4. Thus, the value in the first three columns is 4, which corresponds to vbYesNo. When we call the OR version of UserConfirms, we see both a [Yes] and a [No] button: