C#とC++についてのメモ
※以下記載する内容は調べたものであり、間違った解釈や勘違いも十分にあるため
必ず一度自分でも調べなおしてほしい。
ゲーム開発で使うプログラム言語
ゲーム開発における様々なツールやゲーム内処理(ゲーム内ロジック、通信等)は、
様々な言語の利用によって実装されている。
ツール開発だとPythonやShell Script、通信やDBだとGoやJavaScript、GPU言語(シェーダー)だと HLSLやGLSL...
とまあ本当にいろいろある。
その中でもゲーム開発をするとなると、C#とC++という二つの言語を耳にすると思う。
これらは主に、クライアント/フロントエンド(プレイヤが直接触れるまたは近い部分の実装)でよく利用される言語。
ゲーム開発での活用ゲームエンジン、ライブラリ事例
C++
・UnrealEngine(ゲームロジック周り)
・RE ENGINE(エンジン部分)
・MT Framework
C#
・Unity
・RE ENGINE(ゲームロジック部分)
・FF16開発エンジン
以下参考資料
C++とは…
※ここに記載するのは純粋なC++についてである
汎用プログラミング言語の一つで、C言語から派生した手続き型/データ抽象/オブジェクト指向等といった
設計思想を取り入れた言語である。機械語に変換(コンパイル)する際にアセンブリ言語以外の低水準言語を必要としない
高速な処理のできる言語設計思想で作られている。
ISO/IECが共同で標準規格を3年程度の間隔で発表している。
C#とは…
汎用プログラミング言語の一つで、Microsoftが開発したC/C++に似たの書き方ができる言語。
手続き型/オブジェクト指向/コンポーネント指向等といった設計思想を取り入れた言語
Windowsの「.NET Framework」上で動作することを前提として開発された言語である。
機械語に変換(コンパイル)する際にはCIL(共通中間言語)を介する。
.NET Framework/.NET Coreの更新によって規格も更新されるため発表は不定期で行われる。
近年1年間隔では発表されている。
ここまでを踏まえて
C++はCの派生、C#はC/C++の書き方に似ている。名前が似ている、オブジェクト指向が入っている
といったところからこれら二つの違いをあまり意識しない人もいるのではないかと思うが、
上記あるようにこれらは作ったひと/場所。更新をしている場所が、生み出された経緯が違く中身も結構違うのである。
機械語までの流れ
そもそも機械語とは...
機械語は 数字 で表現される命令列のコードである。
人間には理解が難しいが機械的には解釈が楽でありCPUへの行動命令が羅列されているものである。
プログラム言語を機械語に
・プログラムをビルド/コンパイルしたことがある人
・PCのアプリ(ソフト)をファイルから起動したことがある人
上記経験者はおそらく.exe/.appといった拡張子のついたファイルを見たことがあるだろう。
これが実行ファイル、バイナリ、アプリ/ソフトとよばれるものである。
上記はPCの実行ファイルだが、もちろんゲーム機にも同様の実行ファイルが用意されている。
これらの中身は機械語であり、コンパイルという変換工程をへて人間が書いたC++/C#等のコードを
機械語のコード(命令)に翻訳したもの。
C++の場合のフロー
1. プリプロセス/プリプロセッサ
#include、#define などの記述箇所を該当のコードに展開/置き換えして一つのコードに変換する。
プリプロセッサ定義でコードの仕分けをした場合は、この時に該当コードのみを取り入れる。
※
#include:ヘッダーファイル内容の展開命令定義
#define:マクロの定義 で該当の値や式を記号な名前に置き換える定義
2. コンパイル
C++のコードをアセンブリコードに変換する。
変換の際にコードの解析(パース)、その解析をもとにしたコード最適化、型チェックを行う。
autoやsize_tの型確定をコード解析の結果で行う。
よくビルドの際にコンパイルエラーというものが出ると思うが、あのエラーは
このコンパイルを行うコンパイラがコード解析を行って構文的問題を発見してエラー出力してくれている。
constexprといった、事前計算定義系はこのタイミングで確定する。
3. アセンブル
4. リンク(静的リンク/Static Linking)
既に機械語に翻訳されている静的ライブラリをつなげる。 .lib(Windows)や .a(Linux)のような静的ライブラリをこのタイミングで含める。
C#の場合のフロー
1. C#コンパイル(CIL変換)
C# のコードを解析(パース)とCIL/MSIL(中間言語) コードに変換
CILの動的ライブラリの形式のアセンブリを作成
※このアセンブリとはC++コンパイル時のアセンブリ言語ではなく、
動的ライブラリ的にしたもののことを意味する。
CIL は CPU ではなく .NET ランタイム(CLR)で解釈しやすいコード。
2. JIT/AOT コンパイル(CIL → 一部機械語)
JITは ( Just-In-Time)実行時に実行に必要なコードを機械語にする。
AOTは(Ahead-Of-Time)実行前(ビルド時)に実行に必要なコードを機械語にする
機械語にされた部分はコード最適化される。
3. 実行(動的リンク)
JIt/AOTで機械語にされなかった箇所を実行中に随時機械語に変換する。
動的ライブラリを利用していた場合はこの時に繋げる。
機械語までのフロー比較で見える特徴
C++は事前にすべてのコードを機械語にし、一部のコードもコンパイル時に確定するので、
実行中は出来上がった機械語を使います。
C#のコードは事前にCILにして完全に機械語にはせずに、実行時中/実行中(ランタイム)に機械語に変換されます。
これが良くC++がC#より高速であるという理由の一つです。
機械語に変換する工程や可変的コードが減って、処理負荷が減るということです。
※ただ、近年はAOT等の利用することでC#も十分早くはなってきている。
C#はCILになって利用されます。CILはプラットフォーム依存がないことを思想に作られたもので
JIT/AOTや実行時にそのプラットフォームに合うようにコードが最適化や変換されます。
逆にC++はコンパイル時に確定させるため、プラットフォーム単位にコードを変えて、
プラットフォーム単位に実行ファイルが必要になります。
そのため、複数プラットフォームに対応するのが非常に面倒です。
このように、C++とC#は根本的に構造が違くそれぞれにメリットがある。
機能の違い
型の扱い
C++
auto(型推論)がある。
型を引数やラムダのキャプチャで渡すときに参照渡しにするか、コピー渡しにするか選択ができる。
※ラムダで参照渡しでキャプチャしたものが破棄された場合エラーになるので注意
また、moveによるアドレスをそのままに渡す方法もある。
C#
var(型推論)がある。
全ての型は値型と、参照型に区別される。
ポインタ(メモリ)管理
C++
ポインタを使うとき明示的に生成/破棄(メモリ解放)をするか、C++11より追加されたスマートポインタという 所有権の明記によるポインタの自動破棄管理機能を使う、2つの方法があります。
明示的ポインタ破棄は、記述を忘れるとそのメモリが解放されずにメモリリークにつながる。
生成せずに破棄を明記または、一度削除済みのポインタを破棄といった場合はエラーやクラッシュ、メモリ状態の不安定を招く原因となる。
これは、派生元となったC言語の名残でもあります。
生成をスマートポインタの記述にすることポインタに所有権の概念を与えて、その所有権をもとに自動的に該当の破棄可能なポインタ破棄する。
スマートポインタには種類があるため、使い分ける必要がある。
また、スマートポインタの登場はC++11からだが、C++17、20、23でも機能追加/更新があるので
今後さらに便利になっていくと思われる。
※malloc/free等によるメモリ管理もあります。
スタックメモリ
ローカル変数や関数の引数等の一時的な処理で利用
コピー渡しの値
ヒープメモリ
長期的や任意的なメモリ保持が必要な、明示的な生成(new)やスマートポインタ、
mallocで確保されたオブジェクトや値。
C#(CLR)
GC(ガベレージコレクション)というポインタの破棄管理をユーザーに意識させない、
機能が用意されています。
GCは参照がなくなったものをGCの更新毎にチェックして破棄します。
面倒なメモリ管理を意識しなくてよい分、メモリの解放がGC依存になり場合によっては、
メモリの解放が想定より遅れていたりする場合などはあります。ただ、楽ではあります。
スタックメモリ
ローカル変数や関数等の一時的な処理で利用
値型で利用
※参照型の値をローカルで置いたとき、そのアドレスをスタックして実体はヒープ
ヒープメモリ
参照型で利用
※ラムダ(クロージャオブジェクト)もおそらくそう?
継承
C++
クラスの多重継承をサポート。
そのため該当のクラスに様々なクラスの機能を継承することが可能。
ただ、インターフェースという形は存在しないため、クラスをそういう扱いに疑似的にすることになります。
また、クラスだけでなく構造体(struct)も継承が可能です。
※C++の構造体とクラスは非常に機能が似ています。
C#
クラスの継承は一つのみで、代わりにインターフェースの多重継承が可能。
インターフェースはクラスと違い、変数やコンストラクタ、デストラクタが定義できないというような
制限があり、仮想そのインターフェースの機能である仮想関数やプロパティの定義をすることができる。
まとめ
違いはそれぞれで利用できるライブラリや環境、構文の記述的違いを踏まえると
バージョン単位でそれぞれ変化したりするので、違いを言い出したらほぼ無限にある。
ただ、上記にある内容は比較的基本となる違いであるため知っておくと、これら言語を
利用する上で役に立てることは場合もある。
また、こういった言語タイでの比較や直腸をしることは今後新しい言語が出た時その言語が
どういう使い方をすれば、現状よりパフォーマンスを出せそうかわかる場合もある。
個人的にRustあたりは素晴らしと思っている。
シェーダー言語も汎用プラットフォームなslangあたりの需要がより上がる可能性も高いと思う。