C++のクラス (class
) と構造体 (struct
) は、デフォルトのメンバーアクセスレベルが異なるだけなので、機能的な違いはありません*1。構造体でも、メンバー変数だけでなくメンバー関数を定義することができます。当然コンストラクタやデストラクタをユーザー定義することもできます。
コード例:
#include <string> struct MyStruct { std::string name; // デフォルトコンストラクタで空文字列に必ず初期化されるので、コンストラクタのメンバー初期化子リストに記述する必要はない。 bool isEnabled; bool isChecked; float positionX; float positionY; int orderZ; // ゼロ相当の値で POD 型メンバー変数を初期化。 MyStruct() : isEnabled() , isChecked() , positionX() , positionY() , orderZ() {} // デストラクタをユーザー定義していない場合、コンパイラが自動生成する。 // いずれにせよメンバー変数のデストラクタは自動実行されるため、値型として定義された name メンバーは自動的に破棄される。 }; int main() { // 構造体のデフォルトコンストラクタがユーザー定義されている場合、デフォルト初期化したローカル変数の内容は指定した値で初期化される。 MyStruct s; }
ただ個人的な意見としては、構造体のほうは基本的に複数のpublicデータをまとめて管理するだけの簡素な使い方(Cの構造体やPascalのレコードとほぼ同様の使い方)をするべきだと思っているので、コンストラクタはユーザー定義しないことのほうが多いです。当然デストラクタもユーザー定義しません。デストラクタをコンパイラに自動定義させる場合、デフォルトで非仮想となり、「基底クラスBase
へのポインタと仮想デストラクタを経由して派生クラスDerived
のオブジェクトをdelete
する」ということ(ポリモーフィックな削除)はできないため、構造体は派生型を定義しない方針で運用します(C++11以降は、Cコードと共有する構造体でなければfinal
を付けてしまったほうがよいかも)。C#の構造体は言語仕様レベルで仮想メソッドや派生型を定義できなくなっていますが、C++では運用で実現します。
C# (.NET) では、クラスは参照型、構造体は値型であり、機能および型システム上での明確な違いが存在しますが、使い分けについてのガイドラインもあります。
C++/CLIでは、C#のクラスに相当するref class
/ ref struct
と、C#の構造体に相当するvalue class
/ value struct
を定義することができます。
Javaには構造体相当機能がないものの、Java 16でレコードクラスが追加されました。ちなみにProject Valhallaでは、値クラスvalue class
の導入が検討されているそうです。
Swiftもクラスは参照型、構造体は値型で、明確な違いが存在します。C#の設計が優れていたことが帰納的に証明されました。
グラフィックスプログラミングでよく利用される、2 - 4次元の座標情報をまとめて管理する固定長のベクトル型(float
×2のVector2F
や、double
×3のVector3D
など)は、未来永劫メンバーの数が変わることはないので、こういった変化の小さい汎用的な型に関してはアプリケーションコード側の記述の簡素化のために引数付きコンストラクタや算術演算子オーバーロードを定義することもありますが、将来的な拡張でメンバーが追加されうる構造体に関しては、コンストラクタを書かないほうがむしろ良いのではないかと思います。
もしC++でユーザー定義のコンストラクタやデストラクタが必要になった場合、大抵そのデータ型は構造体ではなくクラスで実現するべき(メンバー変数を隠蔽して、アクセッサーを用意するべき)、ということを示唆しています。特にポインタ型のメンバー変数をpublicにして公開すると、正規の手順を経ることなく外部コードから勝手にでたらめなアドレスを代入したり、ポインタが指している領域を勝手に削除したり、といったことができてしまうため、危険性が増します。
そのコンストラクタ、本当に必要ですか?
C/C++において、組み込みの数値型や任意のポインタ型、あるいはそれらから成る構造体や配列といったPOD型の非静的メンバー変数(インスタンス変数)の内容は、デフォルトでは不定値になります。そのため、クラスの非静的メンバー変数はユーザー定義のコンストラクタにて確実にゼロなどの値で初期化しておくべきです。C++11以降はJavaやC#のようにクラス定義ブロック内でメンバー変数に直接初期値を与えることもできるようになったので、こちらを使う手もあります。ただし、コンストラクタのメンバー初期化子リストで指定した値のほうが優先されるというルールがあるため、この機能を利用する場合は混乱を招かないように注意が必要です。
しかし、そもそもゼロ初期化の構文や値初期化の構文を使えば、ユーザー定義のコンストラクタがなくとも、すべてのPOD型メンバーをゼロ相当の値で初期化することができます。これにより、少なくとも不定値による未定義動作だけは防げます。
#include <memory> struct MyPoint { int x; int y; }; int main() { // 構造体のデフォルトコンストラクタがユーザー定義されていない場合、デフォルト初期化したローカル変数の内容は不定値になる。 MyPoint ptBad1; // 以下もデフォルト初期化となり、メンバー変数の値は不定値になる。 std::unique_ptr<MyPoint> ptBad2(new MyPoint); // 以下はすべてゼロ初期化される。 MyPoint ptGood1a = MyPoint(); MyPoint ptGood1b = {}; MyPoint ptGood1c{}; // C++11 以降。 std::unique_ptr<MyPoint> ptGood2a(new MyPoint()); auto ptGood2b = std::make_unique<MyPoint>(); }
配列や構造体のような集成体を初期化子リストでゼロ初期化するとき、Cの場合は初期化子に先頭メンバーのゼロ指定が最低限必要でしたが、C++の場合は空の初期化子リスト{}
が使えます。CでもGNU拡張としては空の初期化子リストが許可されていましたが、C23では晴れてC++同様に空の初期化子リストが使えるようになるそうです*2。
逆に、ゼロ初期化や値初期化の構文を使ったとしても、コンストラクタをユーザー定義しているにもかかわらず、POD型メンバー変数をそのコンストラクタで初期化し忘れていた場合、そのメンバーの値は不定値になってしまいます。詳しくは以下のエントリでも述べていますが、自分が構造体のコンストラクタを積極的に定義したくない理由のひとつがコレです。
C/C++は未定義動作や未規定動作の落とし穴があちこちに存在する危険な言語なんですが、その恐ろしさを理解していない不用心なプログラマーが多すぎる。コードレビューしていると、POD型ローカル変数を宣言時に初期化せず不定値のまま使い始めているコードのほか、前述のようにコンストラクタをユーザー定義しているにもかかわらず、POD型メンバー変数をそのコンストラクタで初期化し忘れている(メンバー初期化子リストに記述していないだけでなく、コンストラクタ本体ブロックでも何も代入していない)危険なクラスや構造体もよく見かけます。C++の言語仕様上は合法なのでコンパイル自体は通ってしまうのですが、未定義動作を引き起こす危険なコードです。たとえデフォルト初期化されても不定値にしないようにするフールプルーフ目的でコンストラクタをユーザー定義したつもりが、逆に未定義動作のバグや脆弱性を作り込んでしまう原因となるわけです。クラスの場合はコンストラクタをユーザー定義するのが一般的であるため、POD型メンバー変数を追加したときにコンストラクタもメンテナンスすることを真っ先に考えると思いますが、構造体の場合はコンストラクタをユーザー定義するのが一般的ではないため、見落としてメンテナンスを怠ってしまうのだと思われます。現在のコンパイラあるいは静的コード分析ツールはこういった危険なコードに対して、ガイドライン警告やlint警告を出してくれるのですが、残念ながら不用心なプログラマーは有用な警告をすべて無視してしまいます。たとえIDE上で分かりやすく強調(ハイライト)表示されていても、彼らは無視してしまいます。なぜさっき書いたコードがハイライト表示されているのか、ということに対して何の疑問も持たないんでしょうね。自分としては、刃物の使い方を知らない状態で不用意に振り回して欲しくないんですよ。要するに、「うかつにコンストラクタを定義するな、もし定義するならば注意を払って実装しろ。新しいメンバーを追加したらコンストラクタも正しくメンテナンスしろ」と言いたい。
ちなみに未初期化のローカル変数や未初期化のメンバー変数を使用すると、MSVCではデフォルトでC4700のコンパイラ警告が出ます。ユーザー定義のコンストラクタで未初期化のメンバー変数が残っている場合、コンパイラ警告は出ませんが、静的コード分析ツールがC26495の警告を出してくれます。GCCでも-Wuninitialized
や-Wmaybe-uninitialized
のコンパイラ警告が出ますが、ユーザー定義のコンストラクタで未初期化のメンバー変数が残っている場合、警告が出ません。Clangではいずれも警告が出ませんが、Clang-Tidyのcppcoreguidelines-init-variables
やcppcoreguidelines-pro-type-member-init
があります。これらの警告が出ているコードは、致命的な問題を引き起こす危険性が高いので、いっそエラーになるように設定してしまったほうがよいと思います。
- Compiler Warning (level 1 and level 4) C4700 | Microsoft Learn
- Warning C26495 | Microsoft Learn
- clang-tidy - cppcoreguidelines-init-variables — Extra Clang Tools git documentation
- clang-tidy - cppcoreguidelines-pro-type-member-init — Extra Clang Tools git documentation
しかし、変数の宣言時初期化を心がけていれば、フールプルーフのためにコンストラクタをユーザー定義する必要はありません。そもそも、メンバー変数がすべてpublicの構造体を、メンバー変数がすべてprivateのクラスと同じように使えるかのように見せかけること自体が大きな間違いなんです。
構造体のコンストラクタのユースケース1: ゼロ以外の無効値
「ゼロあるいはゼロ相当以外の値をデフォルト時の無効値として使用したい場合」は、構造体のコンストラクタをユーザー定義したくなることがあるかもしれません。たとえばインデックス番号はゼロも有効なので、負数-1
を無効値として使いたい場合などが考えられます。
しかし標準C++のSTLコンテナでは配列インデックスに符号無しのsize_t
型を使うので、無効値として負数は使えません。かといって最大値SIZE_MAX
を無効値とみなすというのは危険です。もしC++において無効な状態を表現できるインデックスデータの変数を定義したい場合は、C++17のstd::optional<size_t>
やboost::optional<size_t>
を使うべきです。std::optional
のデフォルト値はstd::nullopt
、boost::optional
のデフォルト値はboost::none
となるので、これらの型を持つメンバーをコンストラクタで明示的に初期化する必要は特にありません。std::optional
やboost::optional
は、有効な値を持たない状態(無効な状態)がありえることを型情報で明確に示しており、また値を持たない状態で値を取得しようとすると、それぞれstd::bad_optional_access
例外やboost::bad_optional_access
例外をスローしてくれるので安全です。とはいえ、Optional型に有効な値が設定されていても、本当に有効範囲内のインデックスであるかどうか(インデックス値が配列のサイズ以上になっていないか)ということは利用時にチェックするコードを書くべきですが。
一方、C++非標準のAPIや、サードパーティー製のライブラリでは、配列インデックスに符号付きのint
やint32_t
相当を使っているものもあると思います。Optional型よりも軽量でお手軽な数値型を使いたい気持ちは分かりますし、C++以外の他の言語との相互運用*3では単純な数値型のほうが便利ですが、ただの数値型では、無効なインデックス値に何を使うかという外部仕様をいちいちドキュメント化しないといけないし、万が一負数を使って配列にインデックスアクセスしてしまった場合、C/C++では未定義動作を引き起こします。
列挙型のメンバーとしてInvalidやUnknownといった無効値・未詳値を定義する場合、普通はゼロが割り当てられるように最初のメンバーとすることが多いと思いますが、無効値を表すメンバーの内部整数値を-1
などの負数によって明示的に指定してしまっている列挙型もあるかもしれません。一般的にNULLのような無効値はゼロで実装されることが多いという事実上標準があるにもかかわらず、そういうアホな実装をしてしまった邪悪な列挙型を構造体のメンバーとして含める場合も、コンストラクタをユーザー定義したくなることがあるかもしれません。
bool
フラグを構造体のメンバーとして含める場合に、デフォルト値をfalse
ではなくtrue
にするため、コンストラクタをユーザー定義したくなることがあるかもしれません。
そういったケースでも、基本的にはOptional型を使ったほうが幸せになれます。Optionalの値がstd::nullopt
やboost::none
のようなデフォルト値であった場合、列挙定数やフラグを実際に利用するコード側でデフォルト値の扱いを決めればいいだけの話です。クラスは外部に対して一貫性のある振る舞いが必要となるため、内部実装に使われるメンバー変数を隠蔽してカプセル化しますが、一方構造体はあくまで単純なデータを束ねるだけの型であり、各メンバー変数へのアクセス制限はしません。公開メンバーのデフォルト値の設定は、構造体自身に個別に管理させるようなものではないはず。
また、ゼロベクトルやゼロ行列のように、ほとんどの場合デフォルト値はゼロと相場が決まっているので、普通のデフォルトコンストラクタであればゼロ相当の値で初期化されることが直感的に期待されるはずなのに、その慣例に反する初期値を設定してしまうデフォルトコンストラクタを定義するのはいかがなものか。
頻繁に使われるデフォルト設定の集合がある場合、デフォルトコンストラクタではなく専用のファクトリ関数を定義し、明示的に呼び出すようにすればいいだけの話です。デフォルトコンストラクタは1つしか定義できないので、例えばシーンAでよく使われるデフォルト値とシーンBでよく使われるデフォルト値が別々だった場合に対処できず、またデフォルトコンストラクタの実装をうかつに変えると影響範囲が無駄に大きくなってしまいます。
なお、引数付きコンストラクタを定義せず、デフォルトコンストラクタのみを定義しておくと、初期化子リストを使って初期化することができなくなります。
ちなみにC#の構造体はバージョン9まで、引数のないコンストラクタ(デフォルトコンストラクタ)はユーザー定義できず、デフォルトコンストラクタでは構造体のフィールドは必ずその型の既定値で初期化される仕様になっていました(C#では、構造体でもクラスでも、特に指定がないかぎり、数値型や列挙型のフィールドはゼロ相当の値で初期化され、参照型のフィールドはnullで初期化されます)。構造体のデフォルトコンストラクタがユーザー定義できるようになったのはC# 10からです*4。
構造体のコンストラクタのユースケース2: 邪悪なメンバーの初期化
「ユーザー定義されているものの、メンバーを適切に初期化しておらず、不定値になってしまう怠惰なデフォルトコンストラクタを持つ型」をメンバー変数に持つ構造体の場合はどうでしょうか。このようなメンバー変数は、前述のようにゼロ初期化や値初期化の構文を使っても不定値になってしまいます。そのため、構造体にコンストラクタを定義し、メンバー初期化子リストで引数付きコンストラクタを使用して初期化するなどの対処をする必要があります。とはいえ、そもそもこのような怠惰なコンストラクタをユーザー定義するのは邪悪な設計の典型*5であり、ましてや怠惰なコンストラクタを持つ邪悪な型を構造体に含めるべきではありません。どうしてもそのような邪悪な型の変数をメンバーに含めなければならない場合、構造体ではなくクラスとして定義し、そのメンバー変数は外部から直接アクセスできないように隠蔽すべきです。
構造体のコンストラクタのユースケース3: オーバーロード
「構造体に引数付きコンストラクタを定義しておくと、メンバーを追加したときに引数を増やすので、既存コードのコンストラクタ呼び出し箇所がコンパイルエラーとなり、修正すべき箇所が分かりやすくなる」という意見もあるかもしれません。しかし、もしメンバーをN個増やしたと同時にN個減らした場合、コンストラクタの引数の数は変わらないので、場合によっては既存コードのコンストラクタ呼び出し箇所を修正しなくてもコンパイルが(意図せず)通ってしまう可能性もあります。
struct MyPie { double centerX; double centerY; double radiusX; double radiusY; //double sweepAngleDegrees; // 旧メンバー。 double sweepAngleRadians; // 新メンバー。 #if 0 // 旧コンストラクタ。 explicit MyPie(double centerX, double centerY, double radiusX, double radiusY, double sweepAngleDegrees) : centerX(centerX) , centerY(centerY) , radiusX(radiusX) , radiusY(radiusY) , sweepAngleDegrees(sweepAngleDegrees) {} #else // 新コンストラクタ。 explicit MyPie(double centerX, double centerY, double radiusX, double radiusY, double sweepAngleRadians) : centerX(centerX) , centerY(centerY) , radiusX(radiusX) , radiusY(radiusY) , sweepAngleRadians(sweepAngleRadians) {} #endf }; int main() { // degrees で指定する旧コード。そのままでもコンパイル自体は通ってしまい、仕様変更に気づかない。 MyPie pie1(100, 200, 40, 30, 180.0); // C++11 以降は、コンストラクタがユーザー定義されているかどうかにかかわらず、以下のように書くこともできる。 // ただしデフォルトコンストラクタのみが定義され、引数付きコンストラクタが定義されていない場合は不可。 MyPie pie2{100, 200, 40, 30, 180.0}; // コンストラクタがユーザー定義されていない場合、 // あるいは C++11 以降で explicit 指定のない引数付きコンストラクタ(変換コンストラクタ)が定義されている場合、以下のように書くことができる。 //MyPie pie3 = {100, 200, 40, 30, 180.0}; }
Objective-CやSwiftのメソッドは、引数にラベルを付けることができ、オーバーロードは引数の型ではなくラベルによって実現しますが、メソッドのラベル(セマンティクス)を変更するとメソッドインターフェイスの互換性が破壊され、既存のコードがコンパイルエラーになる(破壊的仕様変更になる)ので、修正すべき個所がすぐに分かるようになっています*6。一方、JavaやC#など、C++系統の言語におけるメソッドやコンストラクタは、引数の型が異なる場合にオーバーロードを定義でき、引数の名前はシグネチャに影響しないので、もし引数におかしな名前やスペルミスを含む名前を付けてしまったときでも、メソッドインターフェイスの形式的な(型システム上の)互換性を維持したままこっそりリネームするといったことができますが、当該引数の意味や使い方(仕様)を変えても型が同じ(または暗黙変換可能な型同士)だった場合は既存コードのコンパイルが通ってしまい、修正すべき個所がすぐには分かりません。どちらの言語系統の仕様も、メリットとデメリットがありますが、互換性が破壊されたはずなのに既存コードのコンパイルが通ってしまうのは明らかに問題があります。
C++/Java/C#においてうかつにメソッドやコンストラクタのオーバーロードを定義するのは危険です。公開APIのようにファーストパーティ以外にも広く使われている場合、互換性のない機能追加や仕様変更を実施したときはコンパイルし直してもらう必要がありますが、既存のオーバーロードのせいで身動きがとれなくなってしまう可能性があります。こういうときは引数付きコンストラクタの代わりに異なる名前を持つファクトリ関数を別々に用意したほうがよく、上記のコード例では初期化したMyPie
構造体のインスタンスを戻り値で返すMakeMyPieWithSweepAngleDegrees()
とMakeMyPieWithSweepAngleRadians()
を定義するべきです。オーバーロードとは異なり、型システム上で同じシグネチャを持つ新ファクトリ関数を定義しても、名前が異なる旧ファクトリ関数を維持したままにすることができます。
そもそもdegreeをあとからradianに変えるような互換性破壊をするなよ、というのはもっともですが、特に内部実装でのみ使われている非公開コードにおいて、機能追加や仕様変更によって内部の既存コードの互換性を破壊せざるを得ないことはどうしてもあります。そういった互換性破壊をしたとき、容易に気づけるようなコードになっているかどうか、という点で、クラスや構造体の引数付きコンストラクタのオーバーロードには潜在的な拡張性欠損の問題がある、ということを述べているわけです。なお、APIの外部に公開するデータ型の場合は、メンバー変数を直接公開するようなことはせずに、最初からgetter/setterを用意しておけば、内部で保持するデータの単位や形式を変えても既存コードへの影響を最小限にすることができるので、構造体ではなくクラスとして定義することも検討したほうがよいです(C APIの場合は不透明型とgetter/setter相当の関数を経由したアクセスに限定したり、単位情報や形式情報を保持するタグ付き共用体にしたりする)。
余談ですが、個人的に引数付きのコンストラクタは、コピーコンストラクタとムーブコンストラクタを除いて、一律explicit
を付けるようにしています。explicit
がない場合、引数の型のオブジェクトから暗黙変換することができる「変換コンストラクタ」(converting constructor) となりますが、特に引数が1つだけの場合、意図しない変換による問題を引き起こすことが多いです。なお、C++11以降は、複数の引数を持つコンストラクタにexplicit
がない場合、初期化子リスト{...}
から暗黙変換することもできるようになっています。とはいえ、コンストラクタを呼び出しているのかどうかが判然としないので、個人的には好みではありません。ほとんどの場合、暗黙変換はメリットよりもデメリットのほうが大きすぎて、採用する積極的な理由がありません。
C/C++の構造体の初期化は、初期化子リストを使ってS s = {...};
と書くこともできるので、上記のようにすべてのメンバーに対応する引数を受け取って初期化する引数付きコンストラクタを定義するのは冗長なだけです。ただし初期化子リストの場合、構造体のメンバーの並びを変えたときなどに既存コードの互換性が崩れるので、いったんゼロ初期化してから各メンバー変数に明示的に代入するコードのほうが安全です。コンパイラ最適化が効けば二重初期化のコストはかかりません。C++11以降の一様初期化 (uniform initialization) も同様です。Windows APIのように、バイナリレベルの後方互換性を維持する必要のあるAPIでは、構造体の既存メンバーの並びを変えたり削除したりするようなことはせず、新しいメンバーを追加する場合であっても必ず末尾に追加するようになっていますが、バイナリレベルの後方互換性を維持する必要のないユーザー定義のコードでは、リファクタリングの際に既存の構造体のメンバーの並びを変えたり削除したりすることもあります。構造体に新しくメンバーを追加する場合、不要なパディングが入らないよう、領域節約のためにメンバーの並びを調整したほうがよいこともあります。前述の例のようにメンバー変数を増減させたときも、メンバー変数の名前をコード上で明示しておけば、仕様が変わったときにコンパイルエラーになるため、修正すべき個所がすぐに分かります。メンバーアクセスの記述量は増えますが、リテラルをだらだら並べるよりも遥かに可読性が高いと感じます。
struct MyPie { double centerX; double centerY; double radiusX; double radiusY; //double sweepAngleDegrees; // 旧メンバー。 double sweepAngleRadians; // 新メンバー。 }; int main() { MyPie pie1 = {}; // ゼロ初期化。 //MyPie pie1{}; // C++11 以降はこちらの記法も使える。 pie1.centerX = 100; pie1.centerY = 200; pie1.radiusX = 40; pie1.radiusY = 30; // degrees で指定する旧コード。コンパイルエラーになるので仕様変更に気づきやすい。 pie1.sweepAngleDegrees = 180.0; }
C++20では、C99やC# 3.0以降/VB 9.0 (2008) 以降のように、初期化子リスト内でメンバー名を指定できるようになりました(指示付き初期化: designated initialization)。C99の指示付き初期化と比べると融通が利かず、若干機能面で劣りますが、これはもうC++11でさっさと導入しておけばよかったのではないかと思います*7。
- 指示付き初期化 [P0329R4] - cpprefjp C++日本語リファレンス
- 集成体初期化 - cppreference.com
- Aggregate initialization - cppreference.com
- How to initialize objects by using an object initializer - C# | Microsoft Learn
- Object Initializers: Named and Anonymous Types - Visual Basic | Microsoft Learn
従来の初期化子リストやコンストラクタ呼び出しと違い、各メンバーに明示的代入する方式では、構造体変数をconst
修飾やconstexpr
修飾することができないという欠点がありますが、指示付き初期化は両者の良いところ取りができる優れた方法です。
さらにC++20では、コンストラクタがユーザー定義されていなくても、ブレース{...}
ではなくパーレン(...)
を使って集成体の初期化ができるようになりましたが、前述の理由から指示付き初期化のほうがお勧めです。
再掲:そのコンストラクタ、本当に必要ですか?
上記のように、構造体のコンストラクタをユーザー定義するユースケースはいくつか考えられるものの、逆にメンテナンス性が低下するデメリットのほうが大きいため、やはりユーザー定義すべきではないと考えています。
もうひとつ、構造体のコンストラクタを定義したくない理由のひとつとして、constexprでないコンストラクタをユーザー定義すると、その型のインスタンスはconstexprにできない(コンパイル時定数にできない)ということが挙げられます。constexprは暗黙的にインラインでなければならないので、もし構造体をヘッダーファイルに定義している場合は、そのconstexprコンストラクタの定義をヘッダーに書く必要があります。コーディング規約で関数の実装をヘッダー側ではなくソースファイル側に書くよう定めている場合もあると思いますが、そのようなルールはconstexprコンストラクタと相性が悪いです。constexprコンストラクタを定義するか、あるいはコンストラクタを一切ユーザー定義していなければ、初期化子リストもしくはconstexprファクトリ関数を使ってconstexprインスタンスを生成することができます。
結論:C++を捨てましょう
C++は本当にめんどくさいですよね。でも大丈夫です。世の中には、もっと安全で効率が良く、コンパイルも高速で実行速度も十分な後発プログラミング言語がたくさんあります。ほとんどの場合、あなたの書いているコードはC++である必要はありません。単に古いコード資産を捨てるのが億劫なだけでしょ。必要最低限のものだけ残して、あとは全部捨ててください。C++はバックエンドのためだけにある言語です。フロントエンドをC++で書くなど狂気の沙汰です。
*1:もともとCに毛の生えた「C with Classes」から発展・分化したのがC++なので、いびつで非合理的・不条理とも言える謎の設計が言語仕様のあちこちに散見されます。1980年代当時は仕方なかったとはいえ、今の時代からすると不可解であり、初学者に優しくありません。
*2:なぜこんな単純な構文すら標準Cでは長年許可されなかったのか……
*3:JavaやC#と相互運用する場合は、コレクションのインデックスには32bit符号付き整数型を使うことが多いと思います。JNI の型とデータ構造, .NET Coding Conventions - C# | Microsoft Learn
*4:個人的にはまったく必要性を感じませんが。
*5:古いMFCやD3DXなど、Microsoft謹製のライブラリにはそのような怠惰なコンストラクタを持つ邪悪な型が定義されていることが多く、そういった無作法なコードを真似して量産する輩を生み出す諸悪の根源となってしまっていたのではないかと思います。
*6:Objective-Cは誰もが認める悪魔合体クソ言語の1つで、正直言って素のCのほうがまだマシと思えるひどさですが、このメソッドラベルの仕様に関しては唯一評価できるポイントです。
*7:そもそも20世紀末に策定されたC規格では利用可能だった機能の実現に、10年越しどころか20年以上もかけるなんて、やはりC++標準化委員会は無能集団以外の何物でもありません。