C ++は過去10年間で大きく変化しました。 基本的な型であるstruct、union、enumも変更されました。 今日は、C ++ 11からC ++ 17へのすべての変更を簡単に説明し、C ++ 20を見て、最後に良いスタイルのルールのリストを作成します。
構造体タイプが必要な理由
構造体のタイプは基本です。 C ++コードガイドラインによると、構造体は、不変式に関係のない値を格納するのに最適です。 鮮明な例は、RGBAカラー、2、3、4要素のベクトル、または本に関する情報(タイトル、ページ数、著者、出版年など)です。
ルールC.2:クラスに不変式がある場合、クラスを使用します。 データメンバーが独立して変化する可能性がある場合は、structを使用します
struct BookStats { std::string title; std::vector<std::string> authors; std::vector<std::string> tags; unsigned pageCount = 0; unsigned publishingYear = 0; };
クラスのように見えますが、2つの小さな違いがあります。
- デフォルトでは、構造体の可視性はパブリックで、クラスはプライベートです
- デフォルトでは、structは基本構造/クラスのメンバーをパブリックメンバーとして、クラスをプライベートメンバーとして継承します
// data struct Base { std::string data; }; // Base , `: public Base` struct Derived : Base { };
C ++コアガイドラインによると、structは関数パラメーターの数を減らすのに適しています。 このリファクタリング手法は、「パラメーターオブジェクト」として知られています。
ルールC.1:関連データを構造(構造またはクラス)に整理する
さらに、構造体はコードをより簡潔にすることができます。 たとえば、2Dおよび3Dグラフィックスでは、数よりも2つおよび3つの成分ベクトルを数える方が便利です。 以下のコードはGLMライブラリ( 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); }
構造の進化
C ++ 11では、宣言中にフィールドの初期化が導入されました。
struct BookStats { std::string title; std::vector<std::string> authors; std::vector<std::string> tags; unsigned pageCount = 0; unsigned publishingYear = 0; };
以前は、そのような目的のために、私は自分のコンストラクタを書く必要がありました:
// ! ! struct BookStats { BookStats() : pageCount(0), publishingYear(0) {} std::string title; std::vector<std::string> authors; std::vector<std::string> tags; unsigned pageCount; unsigned publishingYear; };
宣言中の初期化に加えて、問題が発生しました。宣言中にフィールドの初期化を使用する場合、構造リテラルを使用できません。
// C++11, C++14: - pageCount publishingYear // C++17: const auto book = BookStats{ u8" ", { u8" " }, { u8"", u8"" }, 576, 1965 };
C ++ 11およびC ++ 14では、ボイラープレートコードを使用してコンストラクターを記述することにより、これを手動で解決しました。 C ++ 17では、何も追加する必要はありません。標準では、フィールド初期化子を使用して構造体の集約初期化を明示的に許可しています。
この例には、C ++ 11および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; };
C ++ 20では、集計の初期化がさらに改善されると約束されています。 問題を理解するには、以下の例を見て、5つの初期化されたフィールドのそれぞれに名前を付けます。 初期化の順序が混乱していますか? リファクタリング中に誰かが構造体宣言のフィールドを交換したらどうなりますか?
const auto book = BookStats{ u8" ", { u8" " }, { u8"", u8"" }, 1965, 576 };
C11では、構造を初期化するときにフィールド名を指定する便利な機会が導入されました。 この約束は、「指定された初期化子」という名前でC ++ 20に含まれることが約束されています。 詳細については、記事「 C ++ 20への道」を参照してください。
// C++20 const auto book = BookStats{ .title = u8" ", .authors = { u8" " }, .tags = { u8"", u8"" }, .publishingYear = 1965, .pageCount = 576 };
C ++ 17では、「での分解」としても知られる構造化バインディング
宣言」。このメカニズムは、 std::pair
およびstd::tuple
を持つ構造で動作し、集約の初期化を補完します。
// const auto book = BookStats{ u8" ", { u8" " }, { u8"", u8"" }, 576, 1965 }; // const auto [title, authors, tags, pagesCount, publishingYear] = book;
STLクラスと組み合わせることで、この機能によりコードがよりエレガントになります。
#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'; }
ユニオンを入力する理由
実際、C ++ 17では、日常のコードでは必要ありません。 C ++コアガイドラインは、静的な型安全性の原則に基づいてコードを構築することを提案しています。これにより、コンパイラは、率直に誤ったデータ処理中にエラーをスローできます。 共用体の安全な代替としてstd :: variantを使用します。
履歴をリコールする場合、ユニオンを使用すると、同じメモリ領域を再利用して異なるデータフィールドを保存できます。 ユニオン型はマルチメディアライブラリでよく使用されます。 2番目のユニオントークンがそれらで再生されます。匿名ユニオンのフィールドの識別子は外部スコープに分類されます。
// ! ! // 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; }; };
ユニオンの進化
C ++ 11では、独自のコンストラクターでデータ型を統合して追加できます。 ユニオンコンストラクターを宣言できます。 ただし、コンストラクターの存在は正しい初期化を意味しません:次の例では、std :: string型のフィールドはゼロで埋められ、ユニオンを構築した直後に無効になる場合があります(実際、STLの実装に依存します)。
// ! ! union U { unsigned a = 0; std::string b; U() { std::memset(this, 0, sizeof(U)); } }; // - b U u; ub = "my value";
C ++ 17では、バリアントを使用してコードが異なって見える場合があります。 内部では、このバリアントは、ユニオンと大差ない安全でない構造を使用していますが、この危険なコードは、信頼性が高く、十分にデバッグされ、テストされた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>;
列挙型が必要な理由
列挙型は、状態がある場所ならどこでも使用できます。 残念ながら、多くのプログラマーはプログラムロジックに状態を認識せず、enumの使用を認識していません。
以下は、enumの代わりに、論理的に関連するブールフィールドが使用されるコード例です。 m_threadShutdownがtrueでm_threadInitializedがfalseの場合、クラスは正しく機能すると思いますか?
// ! ! class ThreadWorker { public: // ... private: bool m_threadInitialized = false; bool m_threadShutdown = false; };
ここではアトミックが使用されないだけでなく、これはThread*
と呼ばれるクラスで必要になる可能性が最も高いだけでなく、ブールフィールドも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); };
別の例はマジックナンバーです。 4つのスライドのギャラリーがあり、プログラマーがスライドギャラリー用の独自のフレームワークを作成しないように、これらのスライドのコンテンツの生成をハードコーディングすることにしたとします。 次のコードが登場しました。
// ! ! void FillSlide(unsigned slideNo) { switch (slideNo) { case 1: setTitle("..."); setPictureAt(...); setTextAt(...); break; case 2: setTitle("..."); setPictureAt(...); setTextAt(...); break; // ... } }
スライドのハードコードが正当化されたとしても、マジックナンバーを正当化するものは何もありません。 それらは列挙型に簡単に置き換えることができ、少なくとも読みやすさが向上します。
enum SlideId { Slide1 = 1, Slide2, Slide3, Slide4 };
時々、enumはフラグのセットとして使用されます。 これはあまり明確ではないコードを生成します:
// ! - ! 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);
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);
プログラマーは定数をマクロとして書くことがあります。 このようなマクロは、enumまたはconstexprに簡単に置き換えることができます。
列挙規則1:列挙型をマクロより優先する
// ! - 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, };
進化列挙
C ++ 11では、スコープ付き列挙型、別名enum class
またはenum struct
。 この列挙型の変更により、2つの問題が解決されます。
- enumクラスの定数のスコープは、enumクラス自体です。
Enum e = EnumValue1
代わりにEnum e = EnumValue1
Enum e = Enum::Value1
と書く必要があります。 - enumは制限なしで整数に変換され、enumクラスでは、静的キャストが必要
const auto value = static_cast<unsigned>(Enum::Value1)
さらに、enumおよびscoped enumの場合、コンパイラーによって生成されたコードの列挙を表すために使用される型を明示的に選択することが可能になりました。
enum class Flags : unsigned { // ... };
SwiftやRustなどの一部の新しい言語では、デフォルトで列挙型は型変換が厳密であり、定数は列挙型のスコープ内にネストされます。 さらに、以下の例のように、enumフィールドには追加のデータを含めることができます。
// enum Swift enum Barcode { // upc 4 Int case upc(Int, Int, Int, Int) // qrCode String case qrCode(String) }
このような列挙型は、C ++ 2017標準のC ++で導入されたstd::variant
型と同等であるため、このenumが本質的に状態を意味する場合、 std::variant
は構造体およびクラスフィールドのenumを置き換えます。 追加の労力と検証なしで、保存されたデータの不変式への準拠が保証されます。 例:
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>;
良いスタイルのルール
ルールのリストの形式で要約するには:
- C.1:論理的に関連するデータを構造またはクラスに整理する
- C.2:データが不変式によってバインドされている場合、クラスを使用します。 データが独立して変更できる場合は構造体を使用します
- 構造、
std::pair
およびstd::tuple
:auto [a, b, c] = std::tuple(32, "hello"s, 13.9)
変数を宣言するときに分解を使用する
- outパラメーターの代わりに、関数から構造体またはタプルを返します
- フィールド初期化子を指定すると、初期化されていないフィールドがガベージ付きで取得されます
- コンストラクタでゼロでフィールドを初期化しないで、フィールド初期化子に依存します
- 一般に、構造体コンストラクターを記述せず、集約の初期化を使用します
- データが厳密にいくつかの状態のいずれかにあり、いくつかの状態では一部のフィールドが意味を失う場合、構造体またはクラスの代わりに共用
std::variant
の安全な代替としてstd::variant
を使用します -
enum class
またはstd::variant
を使用して、オブジェクトの内部状態を表します
- 異なる状態でクラスが異なるデータフィールドを格納できる場合は、
std::variant
優先します
- 異なる状態でクラスが異なるデータフィールドを格納できる場合は、
- ほとんどの場合、
enum
ではなくenum class
を使用します
- 列挙型から整数型への暗黙的な変換が非常に重要な場合は、古い
enum
使用します - マジックナンバーの代わりに
enum class
またはenum
使用する - 定数マクロの代わりに
enum class
、enum
またはconstexpr
を使用します
- 列挙型から整数型への暗黙的な変換が非常に重要な場合は、古い
関数本体のコードの美しさと簡潔さは、このような些細なことから構築されています。 Laconic機能は、Code Reviewで簡単に確認でき、保守も簡単です。 優れたクラスはそれらから構築され、その後優れたソフトウェアモジュールが構築されます。 その結果、プログラマーは幸せになり、笑顔が顔に咲きます。