Introduction
This article presents a set of templates and macros that, with a minimal amount of code, will provide a way to:
- Associate each enumerator in a C++ enumeration (
enum
) with strings, and convert between string
and enumerator, given either form.
- Associate each enumerator in an
enum
, with an arbitrary set of data (e.g. an int
, and a CRect
paired to each enumerator).
- Iterate (i.e. loop) over the enumeration, in a type-safe and convenient manner (i.e. with a syntax similar to writing a loop for
std::vector
or CArray
).
- Bind an enumerator to MFC Comboboxes and Listboxes (with automatic handling of population, selection and DDX).
- Automatically self-test the
enum
declaration, which helps catch copy/paste errors that may have been made when setting up your declaration.
Under the hood, the actual work is done by a combination of templates, static member functions (some of which are member templates) static class variables, and macros. I initially attempted to code everything with templates, but by the end of it all, I had to resort to the duct tape of C++ (i.e. macros) to clean up the declarations.
Motivation
Enumerations provide a clean, highly readable and type-safe way of dealing with a variable that can take on a well-defined set of possible states (days of the week, or the suits in a deck of cards are the typical examples). This MSDN article gives a good overview of enumerations, but also demonstrates the unfortunate downsides that come with using enum
s:
- Iterating over all of the enumerations with a loop (refer to the second code fragment in the MSDN article, with the
for
loop) requires:
- Use of the names of the first and last enumerators. This isn't a great way to do things - imagine if you add a new enumerator to the beginning or end of the enumeration - you would have to search around and modify all of the loops.
- Writing
operator++
: Writing this operator for each enumeration is mildly inconvenient, but it also requires a potentially unsafe cast. (the results are undefined if you accidentally cast an integer with a value that is not listed in the enumeration).
- If you're saving the value of an enumeration to a file (or registry, etc.) you inevitably end up writing a large
switch
statement (as in the MSDN article) or a chain of if
statements to convert back and forth between the enumerators and a string representation of them.
Amongst other things, the code presented here tries to solve these problems. As an additional level of safety, no casting is performed from integer to enumerator anywhere in the code (all conversions are from enumerator to integer, which is automatic and safe).
Binding to a CCombobox
The following is an example of how to use CEnumBinder
to associate each enumerator in the enumeration with two strings, and bind it to a ComboBox.
The first step in the following code is just a regular C++ enum
declaration. The second step defines the association between each enumerator in the enum
and the corresponding two strings. The first parameter in BIND_ENUM
is the name of the enumeration. The second parameter declares the name of a new enum
binder class (the BIND_ENUM
macro expands to define a class named by the second parameter, so the name of the second parameter can be anything you want).
enum eWeekdays
{
eSunday,
eMonday,
eTuesday,
eWednesday,
eThursday,
eFriday,
eSaturday,
};
BIND_ENUM(eWeekdays, EnumWeekdays)
{
{ eSunday , "sun" , "Sunday" },
{ eMonday , "mon" , "Monday" },
{ eTuesday , "tues" , "Tuesday" },
{ eWednesday, "Wed" , "Wednesday" },
{ eThursday , "thurs", "Thursday" },
{ eFriday , "fri" , "Friday" },
{ eSaturday , "sat" , "Saturday" },
};
Now we add an enum
member variable to the dialog class, and bind the enum
to a CCombobox
:
class CEnumBinderDlg : public CDialog
{
...
CCombobox m_comboBox;
eWeekdays m_enumMember;
}
CEnumBinderDlg::CEnumBinderDlg()
{
m_enumMember = eWednesday;
}
void CEnumBinderDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
DDX_Control(pDX, IDC_COMBO_BOX, m_comboBox);
EnumBinder::DDX_Control(pDX, m_comboBox,
m_enumMember);
}
This produces:
The DDX_Control
function transfers both ways just like a regular DDX. The function takes care of population and selection of the CCombobox
, so once the DDX_Control
is used to bind the member variable to the control, all access is simply done through the m_enumMember
variable.
This works in the same manner as when you bind a CString
to a CEdit
control - you just work with the CString
, before or after calls to UpdateData()
. The default value of m_enumMember
would typically be set in the dialog constructor (or before CDialog::DoModal()
has been called) and the final value would be retrieved as usual (typically, after CDialog::DoModal()
has returned, or in the handler for IDOK
).
Note on CListbox: CListbox
is handled in the exact same manner. For this example, just change the m_comboBox
member from a CComboBox
to a CListbox
, and change IDC_COMBO_BOX
in the dialog editor to a ListBox.
Note on Sorting: in this example, the sort
property of the ComboBox must be set to false
in the dialog editor (or the weekdays will be in alphabetical order, instead of the proper chronological order). For both the CCombobox
and the CListbox
, the sorting is controlled by the settings in the dialog editor. If sorting is set to false
the items appear in the same order as they are listed in the BIND_ENUM
declaration.
The first string is for storing the value to a file, or to the registry. The second string is for display to the user. We will refer to the first string as the code string and the second string as the user string.
Separating the two strings can be quite useful. The first string can be a don't change this value or it will break I/O string, while the second string can be freely changed at any time (e.g. by Documentation writers) without any impact on the file or registry I/O. For example:
Imagine dealing with typos. If a user reports "Wenesday" was spelled incorrectly in version 1.0 of your application, the correct spelling is simply updated in the BIND_ENUM
declaration. Another example where having two strings would be useful is translation. The second string can be changed at runtime, to allow modification of the text in the CEnumBinder
structure, while the first string can, again, be used for file I/O.
Note on UNICODE: Everything will work fine when building with UNICODE
, just wrap all of your strings with the usual _T()
macro. (see the example project for more details).
List of functions (part 1 - Element access)
class CEnumBinder
{
...
int GetSize()
GetAt(Tenum enumItem)
GetAt(int index)
ElementAt(Tenum enumItem)
ElementAt(int index)
...
};
These functions allow you to loop through the enumeration by zero-based integer index, or to lookup the strings using the enumerator. For example:
for(int i=0; i < EnumWeekdays::GetSize(); ++i){
TRACE("\n%s", EnumWeekdays::GetAt(i).m_stringUser);
}
TRACE("\n%s", EnumWeekdays::GetAt(eTuesday).m_stringCode);
TRACE("\n%s", EnumWeekdays::GetAt(eTuesday).m_stringUser);
produces the following output:
Sunday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
tues
Tuesday
Usage note: The GetAt()
functions are preferred to the ElementAt()
functions, as they return const
references. The ElementAt
functions only need to be used if you are changing the strings at runtime (i.e. infrequently).
List of functions (part 2 - String/enumerator conversion)
bool CodeStringToEnum(LPCTSTR stringToCheck,
Tenum& enumValue_OUT)
const CString& EnumToCodeString(const Tenum enumItem)
const CString& EnumToUserString(const Tenum enumItem)
bool CodeStringEnumExchange(CStringOrSimilar& strINOUT,
Tenum& enumINOUT, bool strToEnum)
These functions convert between the enumerator and one of the two strings (the user string, and the code string ). Converting from an enumerator to either string should not fail (unless you pass an enumerator not defined in the BIND_ENUM
declaration), so a string reference is returned directly. Converting from a code string to an enumerator can fail (e.g. trying to match strings coming out of a file, validating data the user typed, etc...), so this function returns a bool
to indicate if a match was found, and returns the enumerator by reference.
A couple of different forms of the functions are provided, for convenience. Refer to the function: CEnumBinderDlg::RegistryExchange
(in EnumBinderDlg.cpp in the example project) for an example of how these functions are used.
List of functions (part 3 - CCombobox/CListBox binding)
bool DDX_Control(bool saveAndValidate,
CListBox_or_CComboBox& listOrComboBox,
Tenum& memberVariable)
bool DDX_Control(CDataExchange* pDX,
CListBox_or_CComboBox& listOrComboBox,
Tenum& memberVariable)
bool Populate(CListBox_or_CComboBox& listOrComboBox)
bool SetSel(CListBox_or_CComboBox& listOrComboBox,
const Tenum selectThisItem)
bool PopulateSetSel(CListBox_or_CComboBox& listOrComboBox,
const Tenum selectThisItem)
bool GetCurSel(CListBox_or_CComboBox& listOrComboBox,
Tenum& itemSelected)
These functions are used for population, control of selection state, and DDX with CComboBox
es and CListbox
es. Refer to the functions: DoDataExchange
and OnLbnSelchangeListCanada
(in EnumBinderDlg.cpp in the example project) for an example of how these functions are used.
Note on member templates: These functions are all C++ member templates. (i.e. the template parameter for the function is figured out automatically by the compiler). This is what allows CCombobox
and CListbox
to be used interchangeably. The really interesting implication of using member templates is that you could use any other class that provides the same set of functions as CComboBox
/CListBox
(AddString, SetCurSel
, etc.), as you will see that there is no use of the class names CCombobox
or CListbox
anywhere in the code.
Automatic self-testing
The class provides automatic self-testing, which ensures there is a one-to-one mapping between the enumerators and the code strings. This is essential because the code strings are written to files or the registry in string form, and when they are read back they must match up to the same enumerator.
The self-test is automatically performed by the class the first time any of the functions are called (in _DEBUG
mode). The self-test can also be run manually by calling the function UnitTest()
, but this is only necessary if you change the code strings at run-time. The user strings are not tested for uniqueness, as this will not cause any errors (although I can't imagine a scenario where this would be a good UI design ?!?).
For example, in the following block of code, any of the last three lines will cause the self-test code to ASSERT
:
enum eFruit {
eApple,
eOrange,
ePear,
ePlum,
eBanana,
};
BIND_ENUM(eFruit, EnumFruit)
{
{ eApple , "apple" , "Apples" },
{ eOrange, "orange", "Oranges" },
{ ePear , "pear" , "Pears" },
{ ePlum , "orange", "Plums" },
{ eApple , "banana", "Banana" },
{ eOrange, "orange", "Oranges" },
};
Extending beyond two strings...
...but I want to associate three strings with my enumerator! (one for the registry, one for the CComboBox
text, and one for a custom tooltip I'm adding to my CCombobox
). As the following example will illustrate, almost any data structure can be associated with an enumerator. We will begin by modifying our weekdays example, and associating a newly defined class to each enumerator.
Adding more than the usual two strings is slightly more complicated, as we have to define a new class (whatever arbitrary data structure you want to associate with the enumeration) and bind it using a new macro called BIND_ENUM_CUSTOM
. We begin, again, by declaring a standard C++ enumeration:
enum eWeekdays
{
eSunday,
eMonday,
eTuesday,
eWednesday,
eThursday,
eFriday,
eSaturday,
};
To add the custom data, we define a new custom-data class (the name of the class is arbitrary). In the following example we will associate a resource ID and an integer with each enumerator. Refer to the function: CEnumBinderDlg::<CODE>OnCbnSelchangeComboWeekdays
(in EnumBinderDlg.cpp in the example project) to see how this is used.
To hook into the CEnumBinder
framework, the custom-data class must contain the macro: INSERT_ENUM_BINDER_ITEMS()
with the parameter to this macro being the name of the enumeration. Typically, the macro is the first listing in the class (i.e. before any other variables), but this ordering can be changed.
class CWeekdaysData
{
public:
INSERT_ENUM_BINDER_ITEMS(eWeekdays);
UINT m_iconID;
int m_offsetX;
};
We need to use BIND_ENUM_CUSTOM
instead of BIND_ENUM
, as we now have to specify the name of the custom data class we just created. The first two parameters to the BIND_ENUM_CUSTOM
macro are the same as the regular BIND_ENUM
macro (name of the enumeration, followed by the name of a new enum
binder class, as explained above). The third parameter is the name of the custom data class, we created in the last code fragment.
We then add the data we want to associate to each enumerator, to the end of each line:
BIND_ENUM_CUSTOM(eWeekdays, EnumWeekdaysCustom, CWeekdaysData)
{
{ eSunday , "sun" , "Sunday" , IDI_WEEKEND, 15 },
{ eMonday , "mon" , "Monday" , IDI_WEEKDAY, 30 },
{ eTuesday , "tues" , "Tuesday" , IDI_WEEKDAY, 45 },
{ eWednesday, "Wed" , "Wednesday" , IDI_WEEKDAY, 60 },
{ eThursday , "thurs", "Thursday" , IDI_WEEKDAY, 75 },
{ eFriday , "fri" , "Friday" , IDI_WEEKDAY, 90 },
{ eSaturday , "sat" , "Saturday" , IDI_WEEKEND, 105 },
};
Note on ordering: The ordering of the entries in the BIND_ENUM_CUSTOM
declaration must correspond to the ordering of variables in the custom-data class. Changing the order of the variables/macro is fine, as long as the class and BIND_ENUM_CUSTOM
declaration are consistent.
In this example, the custom-data class lists the INSERT_ENUM_BINDER_ITEMS()
macro first, followed by the resource ID, and then the integer. This means the enumerator, code string and user string must be the first three entries in each line of the BIND_ENUM_CUSTOM
declaration, followed by the resource ID and then the integer.
Restrictions on enum values
The only absolute restriction on the value assigned to each enumerator in the enumeration is that each one must be unique. However:
Modification and versioning of the enumeration
Adding new entries, or re-ordering the enumerators in the BIND_ENUM
declaration should not cause a problem for reading previously-written files or registry entries, as they are stored in string form. This makes it easy to add new items without causing backwards compatibility problems. Similarly, changing the value of an enumerator should not cause any problems.
Removing entries in the BIND_ENUM
declaration will cause the CodeStringToEnum
function to fail, and the enumerator will be left with whatever value it previously had (the default value?) which is probably what is desired in this situation.
Any of the items elements (The user string, code string, the enumeration, arbitrary data) can be changed at runtime, using the ElementAt()
functions. Refer to the function: OnInitDialog
(in EnumBinderDlg.cpp in the example project) for an example of changing the user string at runtime. Changing any of the other elements are done in a similar fashion.
History
- 2005-08-15: Version 1.0 - Initial release.