📜 ⬆️ ⬇️

Struct, union, and enum types in Modern C ++

C ++ has changed a lot in the last 10 years. Even the basic types have changed: struct, union and enum. Today we will briefly go through all the changes from C ++ 11 to C ++ 17, take a look at C ++ 20 and at the end draw up a list of good style rules.


Why do I need type struct


The type of struct is fundamental. According to the C ++ Code Guidelines, it is better to use struct for storing values ​​that are not related to the invariant. Notable examples are RGBA-color, vectors from 2, 3, 4 elements or information about a book (name, number of pages, author, year of publication, etc.).


Rule C.2: Use class if the class has an invariant; use if this data members can independently

struct BookStats { std::string title; std::vector<std::string> authors; std::vector<std::string> tags; unsigned pageCount = 0; unsigned publishingYear = 0; }; 

It looks like a class, but there are two minor differences:



 //  data  struct Base { std::string data; }; // Base  ,     `: public Base` struct Derived : Base { }; 

According to C ++ Core Guidelines, it is good to use struct to reduce the number of parameters of a function. This refactoring technique is known as the "parameter object".


Rule C.1: Organize related data into structures (structs or classes)

In addition, structures can make the code more concise. For example, in 2D and 3D graphics it is more convenient to count in 2 and 3 component vectors than in numbers. Below is the code that uses the GLM library ( Open GL M athematics )


 //      // . https://en.wikipedia.org/wiki/Polar_coordinate_system glm::vec2 euclidean(float radius, float angle) { return { radius * cos(angle), radius * sin(angle) }; } //     , //     . std::vector<VertexP2C4> TesselateCircle(float radius, const glm::vec2& center, IColorGenerator& colorGen) { assert(radius > 0); //     . //       2. constexpr float step = 2; //     ,     . const auto pointCount = static_cast<unsigned>(radius * 2 * M_PI / step); //  -  . std::vector<glm::vec2> points(pointCount); for (unsigned pi = 0; pi < pointCount; ++pi) { const auto angleRadians = static_cast<float>(2.f * M_PI * pi / pointCount); points[pi] = center + euclidean(radius, angleRadians); } return TesselateConvexByCenter(center, points, colorGen); } 

Evolution struct


In C ++ 11, initialization of fields appeared during the declaration.


 struct BookStats { std::string title; std::vector<std::string> authors; std::vector<std::string> tags; unsigned pageCount = 0; unsigned publishingYear = 0; }; 

Previously, for such purposes, you had to write your own constructor:


 // !   ! struct BookStats { BookStats() : pageCount(0), publishingYear(0) {} std::string title; std::vector<std::string> authors; std::vector<std::string> tags; unsigned pageCount; unsigned publishingYear; }; 

Along with initialization, a problem came up during the declaration: we cannot use the structure literal if it uses field initialization during the declaration:


 // C++11, C++14:    -  pageCount  publishingYear // C++17:   const auto book = BookStats{ u8"  ", { u8" " }, { u8"", u8"" }, 576, 1965 }; 

In C ++ 11 and C ++ 14, this was solved manually by writing a constructor with boilerplate code. You do not need to add anything in C ++ 17 - the standard explicitly allows aggregate initialization for structures with field initializers.


The example contains constructors that are needed only in C ++ 11 and C ++ 14:


 struct BookStats { // !  ! BookStats() = default; // !  ! BookStats( std::string title, std::vector<std::string> authors, std::vector<std::string> tags, unsigned pageCount, unsigned publishingYear) : title(std::move(title)) , authors(std::move(authors)) , tags(std::move(authors)) // ;) , pageCount(pageCount) , publishingYear(publishingYear) { } std::string title; std::vector<std::string> authors; std::vector<std::string> tags; unsigned pageCount = 0; unsigned publishingYear = 0; }; 

In C ++ 20, aggregate initialization promises to be even better! To understand the problem, take a look at the example below and name each of the five initialized fields. Is the initialization order confused? What if someone during the refactoring will swap fields in the structure declaration?


 const auto book = BookStats{ u8"  ", { u8" " }, { u8"", u8"" }, 1965, 576 }; 

In C11, there was a convenient opportunity to specify field names when initializing a structure. This opportunity is promised to be included in C ++ 20 under the name "designated initializer" ("designated initializer"). Read more about this in the article Road to C ++ 20 .


 //    C++20 const auto book = BookStats{ .title = u8"  ", .authors = { u8" " }, .tags = { u8"", u8"" }, .publishingYear = 1965, .pageCount = 576 }; 

In C ++ 17, structured binding, also known as "decomposition at
declaration ". This mechanism works with structures with std::pair and std::tuple and complements aggregate initialization.


 //   const auto book = BookStats{ u8"  ", { u8" " }, { u8"", u8"" }, 576, 1965 }; //   const auto [title, authors, tags, pagesCount, publishingYear] = book; 

In combination with the classes STL, this feature can make the code more elegant:


 #include <string> #include <map> #include <cassert> #include <iostream> int main() { std::map<std::string, int> map = { { "hello", 1 }, { "world", 2 }, { "it's", 3 }, { "me", 4 }, }; //  №1 -   [iterator, bool] auto [helloIt, helloInserted] = map.insert_or_assign("hello", 5); auto [goodbyeIt, goodbyeInserted] = map.insert_or_assign("goodbye", 6); assert(helloInserted == false); assert(goodbyeInserted == true); //  №2 -   [key, value] for (auto&& [ key, value ] : map) std::cout << "key=" << key << " value=" << value << '\n'; } 

Why do we need a union type


In fact, in C ++ 17, it is not needed in everyday code. C ++ Core Guidelines offer to build code on the principle of static type safety, which allows the compiler to give an error when frankly incorrect data processing. Use std :: variant as a safe replacement for union.


If you recall history, the union allows you to reuse the same area of ​​memory to store different data fields. The union type is often used in multimedia libraries. The second union union is played in them: the identifiers of the fields of the anonymous union fall into the external scope.


 // !     ! // Event   : type, mouse, keyboard //  mouse  keyboard      struct Event { enum EventType { MOUSE_PRESS, MOUSE_RELEASE, KEYBOARD_PRESS, KEYBOARD_RELEASE, }; struct MouseEvent { unsigned x; unsigned y; }; struct KeyboardEvent { unsigned scancode; unsigned virtualKey; }; EventType type; union { MouseEvent mouse; KeyboardEvent keyboard; }; }; 

Evolution of union


In C ++ 11, you can add in data types that have their own constructors. You can declare your constructor union. However, the presence of a constructor does not yet mean correct initialization: in the example below, the field of type std :: string is filled with zeros and may well be invalid immediately after the construction of the union (in fact, it depends on the implementation of STL).


 // !     ! union U { unsigned a = 0; std::string b; U() { std::memset(this, 0, sizeof(U)); } }; //    -  b       U u; ub = "my value"; 

In C ++ 17, the code might look different using variant. Inside, a variant uses unsafe constructs that are not much different from union, but this dangerous code is hidden inside a highly reliable, well-established and tested STL.


 #include <variant> struct MouseEvent { unsigned x = 0; unsigned y = 0; }; struct KeyboardEvent { unsigned scancode = 0; unsigned virtualKey = 0; }; using Event = std::variant< MouseEvent, KeyboardEvent>; 

Why do I need the type of enum


The enum type is good to use wherever there are states. Alas, many programmers do not see the states in the logic of the program and do not guess to use enum.


Below is a sample code where logically related boolean fields are used instead of enum. What do you think, will the class work correctly if m_threadShutdown is true and m_threadInitialized is false?


 // !   ! class ThreadWorker { public: // ... private: bool m_threadInitialized = false; bool m_threadShutdown = false; }; 

Not only is atomic not used here, which is most likely needed in a class called Thread* , but you can also replace boolean fields with enum.


 class ThreadWorker { public: // ... private: enum class State { NotStarted, Working, Shutdown }; //   ATOMIC_VAR_INIT    atomic   . //     compare_and_exchange_strong! std::atomic<State> = ATOMIC_VAR_INIT(State::NotStarted); }; 

Another example is the magic numbers, without which, ostensibly, nothing. Suppose you have a gallery of 4 slides, and the programmer decided to work hard on the content generation of these slides, so as not to write your own framework for slide galleries. Such code appeared:


 // !   ! void FillSlide(unsigned slideNo) { switch (slideNo) { case 1: setTitle("..."); setPictureAt(...); setTextAt(...); break; case 2: setTitle("..."); setPictureAt(...); setTextAt(...); break; // ... } } 

Even if the hardcode slide is justified, nothing can justify the magic numbers. They are easy to replace with enum, and this will at least increase readability.


 enum SlideId { Slide1 = 1, Slide2, Slide3, Slide4 }; 

Sometimes enum is used as a set of flags. This generates not very visual code:


 // !   -  ! enum TextFormatFlags { TFO_ALIGN_CENTER = 1 << 0, TFO_ITALIC = 1 << 1, TFO_BOLD = 1 << 2, }; unsigned flags = TFO_ALIGN_CENTER; if (useBold) { flags = flags | TFO_BOLD; } if (alignLeft) { flag = flags & ~TFO_ALIGN_CENTER; } const bool isBoldCentered = (flags & TFO_BOLD) && (flags & TFO_ALIGN_CENTER); 

Perhaps you should use std::bitset :


 enum TextFormatBit { TextFormatAlignCenter = 0, TextFormatItalic, TextFormatBold, //      , //     0,    //     1  . TextFormatCount }; std::bitset<TextFormatCount> flags; flags.set(TextFormatAlignCenter, true); if (useBold) { flags.set(TextFormatBold, true); } if (alignLeft) { flags.set(TextFormatAlignCenter, false); } const bool isBoldCentered = flags.test(TextFormatBold) || flags.test(TextFormatAlignCenter); 

Sometimes programmers write constants in the form of macros. Such macros are easy to replace with enum or constexpr.


Enum.1 rule : prefer enumerated types to macros

 // !   -   C99     ! #define RED 0xFF0000 #define GREEN 0x00FF00 #define BLUE 0x0000FF #define CYAN 0x00FFFF // ,   C99,      enum ColorId : unsigned { RED = 0xFF0000, GREEN = 0x00FF00, BLUE = 0x0000FF, CYAN = 0x00FFFF, }; //  Modern C++ enum class WebColorRGB { Red = 0xFF0000, Green = 0x00FF00, Blue = 0x0000FF, Cyan = 0x00FFFF, }; 

Evolution of enum


In C ++ 11, a scoped enum appeared, aka the enum class or enum struct . This modification of enum solves two problems:



In addition, for enum and scoped enum, it was possible to explicitly select the type used to represent the enumeration in the code generated by the compiler:


 enum class Flags : unsigned { // ... }; 

In some new languages, such as Swift or Rust, the enum type is strict by default in type conversions, and the constants are nested within an enum type scope. In addition, enum fields may carry additional data, as in the example below.


 // enum   Swift enum Barcode { //    upc  4   Int case upc(Int, Int, Int, Int) //    qrCode    String case qrCode(String) } 

Such an enum is equivalent to the type std::variant , included in C ++ in the standard C ++ 2017. Thus, std::variant replaces the enum in the structure and class field, if this enum essentially represents a state. You get guaranteed observance of the stored data invariant without additional efforts and checks. Example:


 struct AnonymousAccount { }; struct UserAccount { std::string nickname; std::string email; std::string password; }; struct OAuthAccount { std::string nickname; std::string openId; }; using Account = std::variant<AnonymousAccount, UserAccount, OAuthAccount>; 

Good style rules


Let's summarize in the form of a list of rules:



From such trifles, the beauty and conciseness of the code in the function bodies is built. Laconic functions are easy to review for Code Review and easy to maintain. They build good classes, and then good software modules. As a result, programmers are happy, smiles are blooming on their faces.


')

Source: https://habr.com/ru/post/334988/


All Articles