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 00:40]
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 notion of [[wp>Enumerated_type|enumeration type]] was added to the C language in the [[wp>ANSI_C|ANSI C]] standard, first published in 1989, after the C language had already been in widespread use for a decade. The variable declaration +===== The problems ===== 
-<code c> +In the 1980'sit was common to use the preprocessor's ''#define'' capability to define //symbolic constants//, as way of documenting the specific semantic interpretation intended for particular numeric values. For exampleone could write:
-enum { kCat, kDog, kCow, kHorse } animal; +
-</code> +
-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'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. +
-<code c> +
-typedef enum { kCat, kDog, kCow, kHorse } Animal; +
-... +
-Animal a1, a2, a3; +
-</code> +
-This //looks// very niceand it's tempting to think that we have succeeded in defining truly new data-type, but in reality this is all just [[wp>Syntactic_sugar|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 integersThat isthe declarations above are entirely equivalent to+
 <code c> <code c>
 #define kCat    0 #define kCat    0
Line 18: Line 9:
  
 int animal; int animal;
-int a1, a2, a3;+ 
 +animal = kDog;
 </code> </code>
-The following code would therefore be quite legitimate, and we quickly see that our hopes of defining a truly new data-type are nothing but empty hopes after all:+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 [[wp>Enumerated_type|enumerated type]] was added to the C language in the [[wp>ANSI_C|ANSI C]] standard, first published in 1989. The variable declaration 
 +<code c> 
 +enum { kCat, kDog, kCow, kHorse } animal; 
 +</code> 
 +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. 
 +<code c> 
 +typedef enum { kCat, kDog, kCow, kHorse } Animal; 
 +... 
 +Animal animal; 
 +</code> 
 +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 [[wp>Syntactic_sugar|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:
 <code c> <code c>
 int x = kDog;        /* assign a symbolic Animal value to an int                   */ int x = kDog;        /* assign a symbolic Animal value to an int                   */
Line 27: 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 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 allowed (and encouraged) a slightly different ''enum'' declaration syntax:+
 <code cpp> <code cpp>
-enum Animal { catdogcowhorse };+enum Animal { kCatkDogkCowkHorse };
 </code> </code>
-which is semantically equivalent to the C ''typedef'' declaration above. This is a bit unfortunatebecause now we have two different syntaxes for the same type declaration, neither of which offers any type-safety. This has been rectified in the [[wp>C%2B%2B11|C++11]] standard, with the introduction of //enum classes//. +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
- +
-For the kinds of C++ programs we write using JUCE, we will almost never need to specify the exact binary representation of symbolic values. C++ offers a subtly different way of using ''enum'', which allows us to define true enumerated data types in a type-safe way. The C++ declaration+
 <code cpp> <code cpp>
-enum Animal { cat, dog, cow, horse };+enum class Animal { cat, dog, cow, horse };
 </code> </code>
-defines a new type ''Animal'', whose binary representation is arbitrary (we neither knownor should we carewhat it is)and group of symbolic value-names which themselves are of type ''Animal''.+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 prefixe.g. ''Animal::cat'', which has the nice result that we could use the name ''cat'' as a constant in any number of ''enum class'' typeswith 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.
  
-bbbbbbbbbbbbbbb+===== 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 55: Line 57:
 } EG_Segment; } EG_Segment;
 </code> </code>
 +This has since been changed to
 +<code cpp>
 +enum class EG_Segment
 +{
 +    idle,
 +    attack,
 +    decay,
 +    sustain,
 +    release
 +};
 +</code>
 +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.
 +
 +===== 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:
 +<code cpp>
 +tyepdef enum
 +{
 +    kSine,
 +    kTriangle,
 +    kSquare,
 +    kSawtooth
 +} SynthOscillatorWaveform;
 +</code>
 +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:
 +<code cpp>
 +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;
 +}
 +</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//.
 +
 +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>
 +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;
 +}
 +</code>
 +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.
  
enum_class_rather_than_typedef_enum.1504226434.txt.gz · Last modified: 2017/09/01 00:40 by shane