ゲーム開発におけるC++の位置づけ
rita
レポートに入る前に、C++の特徴やゲーム開発における位置づけを簡単におさらいします。
2024年現在、C++はゲーム開発において広く使われています。近年はC++以外の言語でゲームを開発できる環境が増えてきた一方で、基盤となるゲームエンジンのメモリ管理や描画処理などの低レイヤー処理には、今でもC++が多く使われています。
ゲーム用のプログラムにC++が用いられる理由として、C++が以下に示す特徴を持つことが挙げられます。
- 最適化された、高速に動作するネイティブコード(※1)を出力する
- C++の前身であるC言語によって記述されたコードを含む、膨大な過去のコード資産を利用できる
- さまざまな世代で提唱されたパラダイム(※2)を併せ持つマルチパラダイム言語である。そのため、用途ごとに最適なプログラミングスタイルを選択できる
※1 CPUが直接解釈できる形式のコード
※2 プログラミング手法を規定する枠組み。手続き型プログラミングやオブジェクト指向プログラミング、関数型プログラミングなどが代表的
ただし、これらは裏返すとデメリットにもなりえます。「高速に動作する代わりにクラッシュの危険性が高いプログラムを書けてしまう」「C言語や過去のバージョン(規格)と互換性があるがゆえに、文法が複雑で簡潔に書くことが難しい」「さまざまな世代の概念を併用できるがゆえに、保守し続けることが難しい」といった具合です。
rita
C++はマルチパラダイム言語として後発の言語で提唱された概念も取り込みつつ、C++自身のメリットを生かすための進化を続けてきました。今回の講演は、C++進化の流れと今後の展望が的確にまとめられていました。
C++における近年のバージョンアップから注目のトピックを紹介
本講演は2部構成で、前半はC++における近年のバージョンアップについて、後半はC++の将来について解説されています。前半では、講演時点での最新バージョンであるC++23から前々バージョンであるC++17までに行われたアップデートの内容が紹介されました。
大幅な規格の改訂が行われた2011年のC++11以降、3年ごとに規格改訂が行われている。なお、鈴木氏らは「CEDEC2020」でC++11から20までのアップデート内容を紹介する講演を行っている
ゲームエンジンやSDKの都合でC++の最新規格が使えない場合であっても、最新の仕様を把握することには以下の観点から大きなメリットがある、と鈴木氏は語りました。
- 過去のバージョンが抱えている問題点に気付き、コードの品質向上に役立てられる
- アップデートの基となった先行実装を過去のバージョンで利用できる可能性がある。先行実装の活用は、将来新しいバージョンに移行するための準備にもなる
- 設計で迷うことがあった場合、新しいバージョンで提唱されている概念に沿うことで、安全かつ効果的に実装が進められる
rita
本稿では、講演で紹介された具体的なアップデート内容の中から、いくつかを抜粋して紹介します。
新しいコア言語機能とメリット
講演冒頭では、「新しいコア言語機能」とそのメリット、注意点が鈴木氏により解説されました。
size_t型を表現するリテラル
プログラミング言語には、特定の型に用途上の意味に応じた別名をつける「型エイリアス」と呼ばれる仕組みが存在します。C++には、サイズを表す型エイリアス「size_t」が定義されています。size_tは環境によって指す型が異なるため、数値をコード上に直接記述する「リテラル」を扱う際、いかなる環境でも正しいコードを書くのに煩雑な手順が必要でした。
size_tの型は、環境に応じて「unsigned int(32bitで表される正の整数)」や「unsigned long(64bitで表される正の整数)」など異なる型を指すため、リテラルを扱いにくい問題があった
これを解決するため、数値の末尾に「uz」をつけることでsize_t型として認識される機能が加わりました。
「5uz」と表記すると、size_t型の「5」として扱われる
標準コンセプト
rita
C++の特徴的な機能に「テンプレート」があります。テンプレートは、任意の型に対する汎用的な実装を定義できる機能です。C++の柔軟さを支える重要な機能であるとともに、仕組みの複雑さが学習や保守における大きなハードルにもなってきました。
複雑さの要因の1つとして、C++20以前ではテンプレートに渡す型を制限できないことが挙げられます。テンプレートに不適切な型を渡して発生するコンパイルエラーによって返されるメッセージは、熟練者であっても原因を読み解きづらいものでした。
C++20より、テンプレートが受け取れる型の特性を記述する「コンセプト」が導入されました。テンプレートが想定する型をコンセプトとして指定することで、不適切な型を渡した際のエラーメッセージが読みやすくなるなどのメリットがあります。
自分で記述したコンセプトのほか、標準ライブラリの中に定義されているコンセプトも利用できる
関数の引数をコンパイル時にチェック
rita
コンパイル時に決定して変化しない値のことを、C++では「コンパイル時定数」として扱います。事前に演算を行うことによる実行時コストの削減や、値が決まっていることによる安全性の保証などの利点から、可能であればコンパイル時定数を使用することが推奨されます。
実行時コスト削減のひとつとして、関数の引数にコンパイル時定数を渡す場合、引数の正当性をチェックする機能が求められていました。
上記の例では、「SaveJPEG」の引数「quality」には、0〜100までの整数値のみ受け付けるようにしたい。「300」や「-200」はコンパイル時定数であるため、コンパイル時にエラーになるのが理想的だ
C++20より、指定した関数を常にコンパイル時評価する「consteval」が導入されました。これにより、コンパイル時に引数の正当性をチェックできるようになりました。
引数用にintをラップした型を定義し、consteval指定したコンストラクタを実装することで、コンパイル時に引数を評価可能。実行時のチェックにかかるコストを削減できる
新しい標準ライブラリ機能
続いて、C++の標準ライブラリに追加された特徴的な機能について、鈴木氏および松村氏が解説を行いました。
std::optionalとモナディック操作
C++17以降では、任意の型に「無効値」の状態を導入できる型「std::optional」(以下optional)が利用できます。
ポインタ型におけるnullptrのように、無効値が代入されている状態を表現できる
optional型変数を読み取る際には、値が有効か否かを判定する条件分岐を書くのが一般的です。そのため、optionalを多用する場合、if文などが増えて読みづらいコードになりやすい問題がありました。
そこで「有効値を持つ場合は処理の結果を返し、無効値だった場合は処理を行わずに無効値を返す」関数を連鎖して呼び出す機能が、C++23より導入されました。これにより、optionalに対する処理をつなげて記述でき、if文やデリファレンスの記述が不要になりました。
関数型言語によく見られる「モナド」の概念を取り入れている。optional型で利用している概念は、特に「Maybeモナド」と呼ばれる
C++23では、有効値を正常系として連鎖させる「and_then」「transform」と、無効値の場合に代替値を返す「or_else」が提供されています。
std::spanによる統一的なメモリアクセス
rita
C++によるプログラムが高速な理由の1つに、ポインタを介した直接的なメモリアクセスができる点が挙げられます。
ただ、ポインタによるメモリアクセスは危険と常に隣り合わせ。プログラムのクラッシュやメモリ内容の破壊、セキュリティホールになるなどさまざまな問題の要因にもなります。
安全にメモリアクセスを行うためには、メモリ範囲を適切に指定する必要がありますが、C++ではメモリ範囲を統一的に扱うことが困難でした。
従来、配列などのメモリ範囲はポインタと要素数によって表現される。この表現には、変数が2つ必要となるデメリットがある
そこで、メモリ範囲を統一的に表現する型として「std::span」(以下、span)がC++20で定義されました。spanは、従来の標準ライブラリに存在していたメモリ範囲を扱う型から変換可能です。
C++17で、文字列の先頭ポインタと長さをセットで受け渡せる「std::string_view」型が定義されました。同様の考え方を、ポインタとサイズを取得可能な任意のコンテナ型に適用できるようにした型がspanです。
rita
C++17以前の環境であっても、メモリ範囲を扱うクラスはstd::spanに対応できるように実装することを推奨します。
文字列フォーマットがコンテナに対応
rita
C++は、標準ライブラリを含め文字列を操作する機能があまり豊富ではありませんでした。コンテナ内の要素を文字列に出力する処理も提供されておらず、プログラマ自身で処理を用意する必要がありました。
C++23より、値を文字列にフォーマットする「std::format」関数(以下、format)で、コンテナ内の全要素をまとめてダンプできるようになりました。
標準ライブラリで定義されているコンテナや、言語機能で記述した固定長配列を文字列化する処理が1行で書けるようになった
また、C言語のsprintfとは異なり、formatはコンパイル時に引数の過不足などをエラーチェックできるため、より安全性が高いメリットもあります。
メモリ範囲を表現するrangesライブラリ
rita
C++11以降では、配列などのコンテナを要素ごとに処理する場合には「範囲 for 文」を使うのがスタンダードな方法です。
範囲 for 文の記述例(cpprefjpより引用)
rita
範囲 for 文は、コンテナから取得できる「イテレータ」というオブジェクトを利用してループ処理の範囲を指定します。「先頭を指すイテレータ」と「終端を指すイテレータ」のペアを取得すると、先頭から終端までの範囲を表現できます。これにより、コンテナ内の要素へ順番にアクセスできます。
1つ前の画像で示した範囲 for 文は、このように書き換えられる(cpprefjpより引用)
先頭と末尾を表すイテレータのペアを「Range(範囲)」として扱い、より柔軟なコンテナへのアクセスを実現する「ranges」ライブラリが、C++20より導入されました。
rangesライブラリが提供するRangeアダプタを利用すると、「逆順にイテレートする」「特定の要素だけを抜き出す」「要素を指定した関数で評価した返り値をイテレートする」などの複雑な要素へのアクセスが簡潔に記述できます。
Rangeアダプタはコンテナを再生成しないため、最小限のオーバーヘッドでアクセス範囲を指定できます。
コンテナに「|」演算子を使ってRangeアダプタを適用する。|によってRangeアダプタを複数適用することも可能
rita
同機能はC#のLINQと似ています。C#におけるIEnumerableとIEnumerableの拡張メソッドが、C++20におけるRangeとRangeアダプタに相当します。
C++に導入が進められている機能と進捗状況
講演の後半では、C++の規格が決まるまでの流れや、検討中の議題に関する現況などが安藤氏より解説されました。
C++の規格が決まるまでのプロセス
C++の規格は、C++標準化委員会の承認によって3年ごとに発行されます。
規格の内容は、専門家の集まるフォーラムでの提案が基になっています。幾度もの議論を重ね、最終的に委員会から採用の価値が認められた提案が、新しい規格に含まれます。
規格が決定するまでの流れ。提案は誰でもできる仕組みとなっている
現在提案されている議案や議論の状況は、GitHubのIssueとしてまとめられており、誰でもアクセス可能です。また、提案文書の日本語訳を公開しているブログも存在します。
導入が検討されている提案
続いて、講演時点で標準化が検討されている提案のいくつかが安藤氏より紹介されました。
ネットワーク機能と非同期処理
講演時点では、「Boost」と呼ばれるライブラリに含まれるネットワークライブラリ「Asio」を標準化する提案を基に、ネットワークライブラリの導入計画が進行しています。
rita
Boostは、C++の先進的な機能や標準ライブラリでは手が届かない部分をフォローするライブラリです。標準規格の先取り候補としても有用です。
Asioの標準化にあたっては、併せて標準化を進めていた非同期処理機構「Executor」の導入を前提としていました。しかし、計画の途中で、より良い設計を持つ非同期処理機構が新たに提案されました。
新しい提案の方が標準規格にふさわしいと判断されたため、標準ネットワークライブラリは実装しなおしとなりました。これにより、計画が大幅に遅延しているとのことです。
もともと計画が進行していたExecutorの提案。実行した非同期処理へのアクセス、処理結果の受け取り、エラーハンドリング、いずれも不可能である欠点を持っていた
新たに提案された非同期処理機構。非同期処理は「sender」と呼ばれる概念によって表現され、結果の取得やキャンセルが可能となる
契約プログラミング
rita
契約プログラミングとは、関数の引数に定められた条件(事前条件)が必ず満たされるようにして、返り値が持つ条件(事後条件)が満たされることを保証する、想定外の動作を未然に防ぐためのプログラミングスタイルです。
C++に導入予定の契約プログラミング機能では、アサート式よりも明示的に満たすべき条件を記述できるほか、記述内容がコンパイラの最適化に反映されます。契約による設計(DesignByContract)を実践しつつ、パフォーマンス向上にも寄与できる仕組みとなっています。
提案されている契約プログラミング機能では、「pre」や「post」を使って事前条件、事後条件を定義する
契約プログラミングの導入に関して、講演時点では、契約違反が起きた際の挙動や、式の評価を制御する方法の仕様を決定するのに難航しているそうです。
パターンマッチング
rita
「パターンマッチング」とは、入力した値に対し、型やデータ構造などの「パターン」によって処理を分岐する機能です。switch文では値と値の一致を調べるのに対し、パターンマッチングでは値とパターンの一致を調べるため、より柔軟性と可読性が高いメリットがあります。
パターンマッチングの例。変数「v」の型にマッチする処理が選択される
パターンマッチングの導入については議論が白熱しており、承認されるのはしばらく先になる見通しとのことです。
リフレクション
rita
「リフレクション」とは、型名・関数名・変数名・メンバなどを文字列として取得し、プログラム上で利用できる機能のことです。
こういった、記述したプログラムの情報を利用して行うプログラミング「メタプログラミング」は、これまでのC++においてはテンプレートを駆使することで実現していました。
ただ、安藤氏は「リフレクションの導入はC++26で実現する可能性が高いだろう」としています。
リフレクションを用いて、列挙型の値を文字列に変換する処理を記述する例
導入予定の機能や最新バージョンの試し方
C++標準機能のうち、ライブラリ機能には基となった実装がオープンソースで公開されている場合があります。formatは「{fmt}」、rangesライブラリは「range-v3」、spanは「gsl::span」が基となっており、これらはC++に導入される以前のバージョンにも対応している可能性があります。
新しい言語機能は、オンラインコンパイラ「Compiler Explorer」で試せるとのこと。Compiler Explorerではさまざまなコンパイラを使用でき、契約プログラミングなど、特定の言語機能を先行実装しているコンパイラも用意されています。
rita
オンラインコンパイラは言語機能や標準ライブラリのちょっとした動作確認や、基本的なロジックの検証にも有用です。
C++23の機能は、主なC++コンパイラで対応が進行しています。C++23に対応したコンパイラは、コンパイラオプションを指定することで利用可能です。
ゲーム業界からC++への提言が発表されている
2023年に、モントリオールのゲーム開発者たちが「C++をゲーム開発でより使いやすい言語にしていくためのレポート」を発表しました。同レポートは、業界内外からのフィードバックを求めているとのことです。
rita
C++の標準化プロセスは万人に開かれているとはいえ、ハードルが高く感じやすいのも事実でしょう。レポートのフィードバックを通じて、ゲーム開発ならではの改善案を届けるチャンスかもしれません。
rita
本稿では、膨大なトピックスが解説された講演の内容から、前提知識を補いつつC++らしい要素に絞って解説しました。一般的に扱うのが難しいとされるC++ですが、進化の様子や見通しを見ていると、まだまだ活躍する言語であると感じました。
なお、本講演のスライドはDocswellにて公開されています。
Docswell「CEDEC 2024『ゲーム開発者のための C++17~C++23, 近年の C++ 規格策定の動向』」cpprefjpゲーム開発者のための C++17~C++23, 近年の C++ 規格策定の動向 - CEDEC2024
ゲームエンジンプログラマ。シリコンスタジオ、ゲームフリークを経て、現在はフリーランス的に活動中。低レイヤ・描画などのランタイムから、ツール・アセットパイプラインまで、ゲームに関する技術はなんでも守備範囲です。RPG・音ゲー・格ゲー・紳士ゲー・お馬さんなどなど幅広く嗜みます。新作を待ちわびているのは『世界樹の迷宮』『ブレイズアンドブレイド』『バーチャロン』など。