GetDunne Wiki

Notes from the desk of Shane Dunne, software development consultant

User Tools

Site Tools


enum_class_rather_than_typedef_enum

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

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]
Line 1: Line 1:
-====== 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>
Line 31: Line 32:
 </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 fineand 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:
Line 38: Line 38:
 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
Line 68: Line 70:
 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:
Line 98: Line 104:
 </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//.
-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 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)
Line 124: Line 133:
 } }
 </code> </code>
-In ''GuiOscTab.cpp'', 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 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 smelland 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.
  
enum_class_rather_than_typedef_enum.1504231017.txt.gz · Last modified: 2017/09/01 01:56 by shane