Both sides previous revision
Previous revision
Next revision
|
Previous revision
|
enum_class_rather_than_typedef_enum [2017/09/01 02:14] shane [Modernizing the VanillaJuce code] |
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 ===== | ===== 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: |
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. |
| |
===== Modernizing the VanillaJuce code ===== | ===== 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> |
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 ===== | ===== Conclusion and analysis ===== |
After eliminating a few old-fashioned ''typedef enum'' declarations and associated code, I didn't reduce my overall line-count---in fact, my code got slightly longer---but I have gained type-safety, and that's a good thing. | 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. |
| |