In the 1980's, it was common to use the preprocessor's #define
capability to define symbolic constants, as a way of documenting the specific semantic interpretation intended for particular numeric values. For example, one could write:
#define kCat 0 #define kDog 1 #define kCow 2 #define kHorse 3 int animal; animal = kDog;
This approach was not what we would now call type-safe, i.e., the compiler would not flag an assignment like animal = 100
as any sort of error, but at least animal = kDog
would be arguably more expressive than the semantically-equivalent animal = 1
.
The notion of enumerated type was added to the C language in the ANSI C standard, first published in 1989. The variable declaration
enum { kCat, kDog, kCow, kHorse } animal;
declares an integer variable animal
, which is supposed to have only four permissible values, identified by the symbolic constant names kCat
, kDog
, kCow
, and kHorse
. It's common to see enum
used together with typedef
, to define an “enum type” (especially in a .h
header file) which can subsequently be used to declare any number of variables with the same set of permissible values, e.g.
typedef enum { kCat, kDog, kCow, kHorse } Animal; ... Animal animal;
This looks very nice, and it's tempting to think that we have succeeded in defining a truly new data-type, but in reality this is nothing but syntactic sugar. The C compiler treats enum
variables just like integers (the number of bits used is compiler-specific). The user-specified symbolic names are assigned actual integer values by the compiler in an ordered sequence, usually starting with 0, and are also treated just like integers. That is, the declarations above are entirely equivalent to the older #define
-based code above.
The following assignments would therefore be quite legitimate, and we quickly see that our hopes of defining a truly new data-type are just empty hopes after all:
int x = kDog; /* assign a symbolic Animal value to an int */ int y = kDog + 99; /* use a symbolic Animal value in an int-valued expression */ animal = 100; /* assign an arbitrary int value to a variable of type Animal */
This is why enum
constant-names are commonly prefixed with a lowercase “k” just like other #define
d constants—because that's basically what they are.
In programming for embedded systems (especially microcontrollers), enum
declarations are often used as a short-form alternative to groups of #define
s, in cases where we would like to use symbolic names for values, but retain the power to specify their exact binary representation. This is fine, and can even be done in C++, but for higher-level programming we would like something a bit cleaner and, well, higher-level.
The original C++ specification introduced a slightly different enum
declaration syntax for defining enum
types without having to use the typedef
keyword:
enum Animal { kCat, kDog, kCow, kHorse };
Unfortunately, this is semantically equivalent to the C typedef
declaration above, so now we have two different syntaxes defining an enumerated type, neither of which offers any type-safety. This has finally been rectified in the C++11 standard, with the introduction of enum classes. In C++11 and later, we can write
enum class Animal { cat, dog, cow, horse };
This defines a new data-type Animal
, and a group of permissible symbolic-constant values, which work in a type-safe fashion. The “k” prefix is no longer needed, because the class
declaration puts these symbols into their own namespace, which we have to use as a prefix, e.g. Animal::cat
, which has the nice result that we could use the name cat
as a constant in any number of enum class
types, with no chance of getting confused among them. The C++ compiler will enforce type-safety rules, making it illegal to try to assign, say, a plain integer value to a variable of Animal
type.
In an early version of VanillaJuce, the file SynthEnvelopeGenerator.h
included the declaration
typedef enum { kIdle, kAttack, kDecay, kSustain, kRelease } EG_Segment;
This has since been changed to
enum class EG_Segment { idle, attack, decay, sustain, release };
and all uses of the symbolic names idle
, attack
, etc. are now changed to e.g. EG_Segment::idle
.
Moreover, I realized that the EG_Segment
data type and values are not even used anywhere outside the SynthEnvelopeGenerator class, so I was able to move the entire enum class
declaration inside the SynthEnvelopeGenerator class declaration—into the private
part, in fact.
This was a nice simple case, because an EG_Segment
is a true categorical variable—an element drawn from a finite set of value-items which have no special relationship other than their shared set membership.
I had also used a typedef enum
declaration in SynthParameters.h
, so I could use the declared type in a few different .cpp
files:
tyepdef enum { kSine, kTriangle, kSquare, kSawtooth } SynthOscillatorWaveform;
When I converted this to the newer enum class
form, however, lots of code broke, and I quickly realized that I had fallen back on my old embedded C habits of assuming the symbolic names kSine
, etc. could be treated like integers. In SynthParameters.cpp
, I had the following:
const char* WFname[] = { "Sine", "Triangle", "Square", "Sawtooth" }; static SynthOscillatorWaveform LookupWF(String wfname) { int wfIndex = 0; for (int i = 0; i < 4; i++) { if (!strcmp(wfname.toUTF8(), WFname[i])) { wfIndex = i; break; } } return (SynthOscillatorWaveform)wfIndex; }
I had also used constructions like String(WFname[osc1Waveform])
to convert the value of a SynthOscillatorWaveform
variable (in this case osc1Waveform
) to a juce::String.
My first thought was that maybe I could add Serialize and Deserialize member functions to the new SynthOscillatorWaveform
class, but alas, fancy new standards notwithstanding, C++ is still up to its old tricks of reusing keywords—in this case, class
—in new ways that don't quite mean what we might expect based on experience. It turns out that C++11's “enum classes” are not classes at all, and hence we can't augment them with member functions.
I ended up just adding a couple of free-standing functions in SynthParameters.cpp
, for serializing and deserializing SynthOscillatorWaveform
values:
String Serialize_SynthOscillatorWaveform(SynthOscillatorWaveform wf) { if (wf == SynthOscillatorWaveform::sine) return "Sine"; if (wf == SynthOscillatorWaveform::triangle) return "Triangle"; if (wf == SynthOscillatorWaveform::square) return "Square"; if (wf == SynthOscillatorWaveform::sawtooth) return "Sawtooth"; // Did we define a new waveform type and forget to add it here? jassertfalse; return "Sine"; } SynthOscillatorWaveform Deserialize_SynthOscillatorWaveform(String wfString) { if (wfString == "Sine") return SynthOscillatorWaveform::sine; if (wfString == "Triangle") return SynthOscillatorWaveform::triangle; if (wfString == "Square") return SynthOscillatorWaveform::square; if (wfString == "Sawtooth") return SynthOscillatorWaveform::sawtooth; // Did we get something unexpected in our input string? jassertfalse; return SynthOscillatorWaveform::sine; }
Then I found I had to write about the same amount of new code, toconvert between SynthOscillatorWaveform
values and integer indices for the juce::ComboBox controls used to select oscillator waveforms. This code was starting to smell, and the more I thought about it, the more uncomfortable I was with the fact that I had defined the human-readable string names for waveform types in four places: once in each of my new serialize/deserialize functions, and once for each of the juce::ComboBox controls in GuiOscTab.cpp
:
waveformCB1->addItem (TRANS("Sine"), 1); waveformCB1->addItem (TRANS("Triangle"), 2); waveformCB1->addItem (TRANS("Square"), 3); waveformCB1->addItem (TRANS("Sawtooth"), 4); ... waveformCB2->addItem (TRANS("Sine"), 1); waveformCB2->addItem (TRANS("Triangle"), 2); waveformCB2->addItem (TRANS("Square"), 3); waveformCB2->addItem (TRANS("Sawtooth"), 4);
(The above code was automatically generated by the Projucer.) Clearly, I needed to re-think the whole notion of waveform selection.
The notion of waveform has more to it than just “an element of a finite set” (which is what a C++11 “enum class” models):
The set of available waveforms in a synthesizer is essentially an ordered collection of objects which have at least three attributes: a unique integer index (basis of the ordering), a human-readable name, and some associated sample-generating code. I realized that my original instinct to use an integer-indexed representation, with a static array of human-readable names, was correct; I just hadn't implemented it very cleanly.
I decided to create a new class SynthWaveform to encapsulate the notion of an integer waveform index and the relationship between indices and human-readable names, and allow only the SynthOscillator class to make direct use of the index type (for efficient selection of sample-generating code). Here is SynthWaveform:
class SynthWaveform { private: enum WaveformTypeIndex { kSine, kTriangle, kSquare, kSawtooth, kNumberOfWaveformTypes } index; friend class SynthOscillator; public: // default constructor SynthWaveform() : index(kSine) {} // set to default state after construction void setToDefault() { index = kSine; } // serialize: get human-readable name of this waveform String name(); // deserialize: set index based on given name void setFromName(String wfName); // convenience funtions to allow selecting SynthWaveform from a juce::comboBox static void setupComboBox(ComboBox& cb); void fromComboBox(ComboBox& cb); void toComboBox(ComboBox& cb); private: // waveform names: ordered list of string literals static const char* const wfNames[]; }; const char* const SynthWaveform::wfNames[] = { "Sine", "Triangle", "Square", "Sawtooth" }; void SynthWaveform::setFromName(String wfName) { for (int i = 0; i < kNumberOfWaveformTypes; i++) { if (wfName == wfNames[i]) { index = (WaveformTypeIndex)i; return; } } // Were we given an invalid waveform name? jassertfalse; } String SynthWaveform::name() { return wfNames[index]; } void SynthWaveform::setupComboBox(ComboBox& cb) { for (int i = 0; i < kNumberOfWaveformTypes; i++) cb.addItem(wfNames[i], i + 1); } void SynthWaveform::fromComboBox(ComboBox& cb) { index = (WaveformTypeIndex)(cb.getSelectedItemIndex()); } void SynthWaveform::toComboBox(ComboBox& cb) { cb.setSelectedItemIndex((int)index); }
I have used a traditional C++ enum
type declaration (without the archaic typedef
), with full awareness of its near-equivalence to int
and consequent lack of type-safety. I have mitigated the risk considerably, however, by making the SynthWaveform type private
, accessible only within this class and to the friend
class SynthOscillator, which has a genuine pragmatic reason to use it (for efficient selection of code at runtime):
float SynthOscillator::getSample() { float sample = 0.0f; switch (waveform.index) { case SynthWaveform::kSine: sample = (float)(std::sin(phase * 2.0 * double_Pi)); break; case SynthWaveform::kSquare: sample = (phase <= 0.5) ? 1.0f : -1.0f; break; case SynthWaveform::kTriangle: sample = (float)(2.0 * (0.5 - std::fabs(phase - 0.5)) - 1.0); break; case SynthWaveform::kSawtooth: sample = (float)(2.0 * phase - 1.0); break; } phase += phaseDelta; while (phase > 1.0) phase -= 1.0; return sample; }
A C++11 “enum class” is an excellent, type-safe replacement for “typedef enum” when dealing with what amounts to categorical variables, which are just symbolic names drawn from a finite, unordered set.
For waveform selection in VanillaJuce, I realized that I was dealing with something considerably more complicated. My first attempt, to blindly apply the substitution typedef enum
→ enum class
resulted in increased line-count, and served to highlight what was already messy code. By re-thinking the representation deeply, I was able to achieve reduced line-count and cleaner code.