Both sides previous revision
Previous revision
Next revision
|
Previous revision
|
enum_class_rather_than_typedef_enum [2017/09/01 01:56] shane |
enum_class_rather_than_typedef_enum [2017/09/01 14:01] (current) shane [Conclusion and analysis] |
====== C++ enum declaration vs. C "typedef enum" ====== | ====== C++ "enum class" vs. C "typedef enum" ====== |
| ===== The problems ===== |
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: | 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: |
<code c> | <code c> |
</code> | </code> |
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. | 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 [[wp>Embedded_system|embedded systems]] (especially [[wp>Microcontroller|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//. |
In programming for [[wp>Embedded_system|embedded systems]] (especially [[wp>Microcontroller|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 entirely valid, even when carried over to C++. | |
| |
The original C++ specification introduced a slightly different ''enum'' declaration syntax for defining ''enum'' types without having to use the ''typedef'' keyword: | 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 }; | enum Animal { kCat, kDog, kCow, kHorse }; |
</code> | </code> |
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 been rectified in the [[wp>C++11|C++11]] standard, with the introduction of //enum classes//. In C++11 and later, we can write | 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 [[wp>C++11|C++11]] standard, with the introduction of //enum classes//. In C++11 and later, we can write |
<code cpp> | <code cpp> |
enum class Animal { cat, dog, cow, horse }; | enum class Animal { cat, dog, cow, horse }; |
</code> | </code> |
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 [[wp>Namespace|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. | 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 [[wp>Namespace|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. |
| |
| ===== Applying enum class in VanillaJuce: case 1 ===== |
| |
In an early version of VanillaJuce, the file ''SynthEnvelopeGenerator.h'' included the declaration | In an early version of VanillaJuce, the file ''SynthEnvelopeGenerator.h'' included the declaration |
and all uses of the symbolic names ''idle'', ''attack'', etc. are now changed to e.g. ''EG_Segment::idle''. | 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. | 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. |
| |
| ===== Applying enum class in VanillaJuce: case 2 ===== |
| |
I had also used a ''typedef enum'' declaration in ''SynthParameters.h'', so I could use the declared type in a few different ''.cpp'' files: | I had also used a ''typedef enum'' declaration in ''SynthParameters.h'', so I could use the declared type in a few different ''.cpp'' files: |
</code> | </code> |
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//. | 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//. |
I cleaned this up by adding a couple of simple functions in ''SynthParameters.cpp'', for serializing and deserializing ''SynthOscillatorWaveform'' values: | |
| 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: |
<code cpp> | <code cpp> |
String Serialize_SynthOscillatorWaveform(SynthOscillatorWaveform wf) | String Serialize_SynthOscillatorWaveform(SynthOscillatorWaveform wf) |
} | } |
</code> | </code> |
In ''GuiOscTab.cpp'', I needed to create similar code to convert between ''SynthOscillatorWaveform'' values and integer indices for the //juce::ComboBox// controls used to select oscillator waveforms, but this was straightforward. | 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'': |
| <code 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); |
| </code> |
| (The above code was automatically generated by the Projucer.) |
| Clearly, I needed to re-think the whole notion of waveform selection. |
| |
| ===== Re-thinking waveform selection in VanillaJuce ===== |
| 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 waveforms might not always be finite. What if, down the road, I were to add some facility for users to define their own? |
| * For humans, waveforms are identified by //string constants//, i.e., their names, typically through GUI widgets like //juce::ComboBox// which have an inherent linear //order//. These names are also useful when serializing preset-parameters to XML, because the names remain valid even if the inherent order of the chosen binary representation should change. |
| * For program code, a more compact, unambiguous representation is needed, e.g. for my //SynthOscillator::getSample()// function, which is called //very often// and needs a quick way to look up the right chunk of code for the selected waveform. |
| |
| 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//: |
| <code cpp> |
| 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); |
| } |
| </code> |
| 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): |
| |
| <code cpp> |
| 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; |
| } |
| </code> |
| |
| ===== Conclusion and analysis ===== |
| 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. |
| |