finm137.wiki
個人的C++仕様書
最終更新:
Bot(ページ名リンク)
-
view
- 概要
- 結論
- 3Dトポロジーとの比較
- C++ファイルとは
- 基本的な.hと.cpp
- 宣言と定義
- ODR
- 翻訳単位
- なぜファイルを分けるのか
- 悪いコード例1 全部main.cpp
- 良い分離例1
- 悪いコード例2 God Class
- 良い分離例2
- 悪いコード例3 巨大関数
- 良い分離例3
- 悪いコード例4 複数責務
- 悪いコード例5 グローバル変数だらけ
- 改善例
- 悪いコード例6 生new乱用
- 改善例 RAII
- 所有権とは
- 悪いコード例7 所有権不明
- 改善例
- 寿命
- 改善例
- 悪いコード例8 範囲外アクセス
- 改善例
- 悪いコード例9 nullptr
- 改善例
- 悪いコード例10 空コード
- 空コードを見抜く
- 悪いコード例11 存在しないAPI
- 確認方法
- 悪いコード例12 エラー握り潰し
- 改善例
- さらに良い例
- 悪いコード例13 循環依存
- 循環依存を減らす
- 依存関係の方向
- 悪い例 OpenGL直結
- 改善例
- インターフェース
- カプセル化
- const
- 悪い例 constなし
- 関数の引数
- 命名
- boolだけでは情報不足な場合
- さらに詳細な結果型
- ログ
- ログレベル
- 3Dにおけるポール集中とC++
- 3DにおけるエッジループとC++
- イベントシステム
- コマンドパターン
- 悪いUndo
- 改善Undo
- ファイル構造の攻略例
- main.cppの役割
- Applicationの役割
- Rendererの役割
- ModelImporterの役割
- ファイル形式を分離する
- 依存関係
- 依存関係のバージョン
- 依存関係を固定する
- dependencies.txt例
- CMake
- CMake基本構造
- ライブラリ接続
- PRIVATE
- PUBLIC
- INTERFACE
- CMakeも構造化する
- target単位で管理する
- なぜtarget単位がよいか
- 3Dの綺麗なトポロジーに相当するCMake
- 悪い依存構造
- リファクタリング
- テスト
- 悪いテスト構造
- 良いテスト構造
- デバッグ
- コンパイルエラー
- リンクエラー
- 実行時クラッシュ
- 警告を無視しない
- コメント
- README
- 設計メモ
- 初心者向け攻略順
- 攻略の鉄則
- 3D自動生成との共通点
- コード品質の判断
- 悪いコードチェックリスト
- 良いコードチェックリスト
- 重要
- 本当に重要なこと
- 最終的な考え方
- まとめ
- 一言で覚える
概要
C++において、
「3Dモデリングにおけるトポロジーに相当するものは何か?」
という疑問に対して、
完全に1対1で対応するC++用語は存在しない。
完全に1対1で対応するC++用語は存在しない。
しかし、実際のソフトウェア開発では、
- ファイル分割
- クラス構造
- 関数の責務
- 依存関係
- データの所有権
- オブジェクトの寿命
- モジュール間の接続
- include関係
- ライブラリ構成
- CMakeターゲット構造
などを含めた
「プログラム全体の構造設計」
が、3Dモデリングにおけるトポロジーにかなり近い。
3Dモデルでは、
「見た目が同じでもトポロジーが悪ければ、
リグを入れたり変形させたりした時に破綻する」
リグを入れたり変形させたりした時に破綻する」
という問題がある。
C++でも同じように、
「一応動いているコードでも、
構造が悪ければ機能追加・修正・デバッグ時に破綻する」
構造が悪ければ機能追加・修正・デバッグ時に破綻する」
という問題がある。
つまり、
3D:
見た目が完成 ↓ でもトポロジーが悪い ↓ 動かしたら破綻
C++:
プログラムが起動 ↓ でも構造が悪い ↓ 機能追加したら破綻
という非常によく似た関係が存在する。
結論
C++で3Dトポロジーに一番近いものは、
「依存関係を含めたプログラム構造」
である。
特に重要なのは、
- 何をどのファイルへ置くか
- どのクラスが何を担当するか
- どのモジュールがどのモジュールへ依存するか
- 誰がデータを所有するか
- どこからどこまで生存するか
である。
単に
.cppファイルを大量に分ければ良い
という意味ではない。
適切な責務ごとに分け、
必要な場所だけ接続する必要がある。
必要な場所だけ接続する必要がある。
3Dトポロジーとの比較
理解用の例。
| 3Dモデリング | C++ |
| 頂点 | 変数・データ |
| エッジ | 関数呼び出し・依存関係 |
| 面 | クラス・モジュール |
| エッジループ | 処理やデータの流れ |
| ポール | 依存関係が集中する場所 |
| 関節 | モジュール境界 |
| トポロジー | ソフトウェア構造 |
| リトポロジー | リファクタリング |
| Non-Manifold | 矛盾した状態や壊れた依存関係 |
| 巨大N-gon | 巨大関数・巨大クラス |
| 変形破綻 | 変更時に発生するバグ |
| リグ | 制御ロジック |
| ウェイト | 責務や影響範囲 |
| UV整理 | インターフェース整理 |
| 自動リトポ | 自動コード生成 |
| 綺麗なQuad | 読みやすいコード |
| 動くためのQuad | 保守可能な設計 |
※これは理解用の比喩であり、
C++仕様上の正式な対応関係ではない。
C++仕様上の正式な対応関係ではない。
C++ファイルとは
C++では主に、
- .cpp
- .h
- .hpp
などを使用する。
.cpp
主に実装を書く。
例:
main.cpp Renderer.cpp AudioEngine.cpp
.h
主に宣言を書くヘッダーファイル。
例:
Renderer.h AudioEngine.h
.hpp
C++専用ヘッダーであることを明確にするため、
.hppを使うプロジェクトもある。
.hppを使うプロジェクトもある。
.hか.hppかはプロジェクト規約による。
基本的な.hと.cpp
例としてRendererを作る。
Renderer.h
#pragma once
class Renderer
{
public:
bool Initialize();
void Render();
void Shutdown();
};
Renderer.cpp
#include "Renderer.h"
bool Renderer::Initialize()
{
return true;
}
void Renderer::Render()
{
}
void Renderer::Shutdown()
{
}
main.cpp
#include "Renderer.h"
int main()
{
Renderer renderer;
if (!renderer.Initialize())
{
return 1;
}
renderer.Render();
renderer.Shutdown();
return 0; }
.h側には、
「何ができるクラスか」
を書く。
.cpp側には、
「どうやって実行するか」
を書く。
宣言と定義
C++では、
「存在を知らせること」
と
「実際の中身を書くこと」
が別になる。
宣言
int Add(int a, int b);
これは、
「Addという関数があります」
という宣言。
定義
int Add(int a, int b)
{
return a + b;
}
これは、
「Addは実際にこう動きます」
という定義。
ODR
C++には
One Definition Rule
略してODRがある。
プログラム内で定義をどのように配置できるかを決める重要なルール。
例えば通常の非inline関数を、
複数の.cppへ勝手に同じように定義すると問題になる。
複数の.cppへ勝手に同じように定義すると問題になる。
悪い例:
A.cpp
int Add(int a, int b)
{
return a + b;
}
B.cpp
int Add(int a, int b)
{
return a + b;
}
このように、
同じ外部リンケージの関数定義を複数作ると問題になる。
同じ外部リンケージの関数定義を複数作ると問題になる。
基本は、
Add.h
int Add(int a, int b);
Add.cpp
#include "Add.h"
int Add(int a, int b)
{
return a + b;
}
とする。
翻訳単位
C++では、
.cppファイルは#includeされた内容などを前処理した後、
.cppファイルは#includeされた内容などを前処理した後、
「翻訳単位」
としてコンパイルされる。
イメージ:
main.cpp ↓ #include "Application.h" ↓ #includeされた内容を前処理 ↓ 翻訳単位 ↓ コンパイラ ↓ オブジェクトファイル ↓ リンカー ↓ exe
そのため、
「ヘッダをincludeできた」
だけでは完成ではない。
最終的にはリンクまで成功する必要がある。
なぜファイルを分けるのか
小さいプログラムなら、
全部main.cppに書くことも可能。
全部main.cppに書くことも可能。
しかし、大型アプリで以下を全部main.cppへ書くとする。
main.cpp ├─ ウィンドウ作成 ├─ OpenGL ├─ モデル読み込み ├─ FBX ├─ VRM ├─ 音声 ├─ AI ├─ UI ├─ Undo ├─ ファイル保存 ├─ 設定 ├─ ログ └─ エラー処理
これでは巨大化する。
これは3Dでいうと、
「顔、腕、足、髪、装飾、
全部のトポロジーを無秩序に1ヶ所へ集中させる」
全部のトポロジーを無秩序に1ヶ所へ集中させる」
ような状態。
悪いコード例1 全部main.cpp
悪い例:
#include <iostream> #include <vector> #include <string>
int main()
{
// ウィンドウ初期化
// GPU初期化
// モデル読み込み
// FBX読み込み
// VRM読み込み
// UI描画
// 音声処理
// AI処理
// Undo処理
// ファイル保存
// 設定ファイル読み込み
// エラー処理
// ログ
// 終了処理
return 0; }
最初は楽。
しかし後で、
「VRM読み込みだけ直したい」
となっても、
巨大main.cppを探す必要がある。
巨大main.cppを探す必要がある。
さらに複数人開発では、
全員がmain.cppを編集することになり、
競合しやすい。
全員がmain.cppを編集することになり、
競合しやすい。
良い分離例1
役割ごとに分ける。
src/ ├─ main.cpp │ ├─ app/ │ ├─ Application.h │ └─ Application.cpp │ ├─ render/ │ ├─ Renderer.h │ └─ Renderer.cpp │ ├─ model/ │ ├─ Model.h │ ├─ Model.cpp │ ├─ ModelImporter.h │ └─ ModelImporter.cpp │ ├─ audio/ │ ├─ AudioEngine.h │ └─ AudioEngine.cpp │ ├─ ai/ │ ├─ LLMEngine.h │ └─ LLMEngine.cpp │ ├─ undo/ │ ├─ UndoManager.h │ └─ UndoManager.cpp │ └─ ui/ ├─ MainUI.h └─ MainUI.cpp
main.cppは起動だけ担当。
#include "app/Application.h"
int main()
{
Application app;
if (!app.Initialize())
{
return 1;
}
app.Run();
app.Shutdown();
return 0; }
悪いコード例2 God Class
God Classとは、
「何でも1クラスで担当してしまう巨大クラス」
のような状態。
悪い例:
class Application
{
public:
void Run();
void CreateWindow();
void Render3D();
void LoadFBX();
void LoadVRM();
void PlayAudio();
void GenerateAIText();
void SaveProject();
void LoadProject();
void Undo();
void Redo();
void DrawUI();
void DownloadFile();
void ParseJSON();
void CompileShader();
void ImportTexture();
};
Applicationという1クラスが、
何でも知っている。
何でも知っている。
3Dで例えるなら、
全エッジが1つのポールへ集中している
ような状態。
良い分離例2
class Application
{
public:
void Run();
private:
Renderer renderer;
AudioEngine audio;
ModelManager modelManager;
UIManager ui;
UndoManager undo;
};
Rendererは描画だけ。
class Renderer
{
public:
void BeginFrame();
void RenderScene();
void EndFrame();
};
AudioEngineは音声だけ。
class AudioEngine
{
public:
bool LoadSound();
void Play();
};
ModelManagerはモデルだけ。
class ModelManager
{
public:
bool LoadModel();
void UnloadModel();
};
Applicationは、
「各機能をまとめて動かす」
だけにする。
悪いコード例3 巨大関数
悪い例:
void LoadModel(const std::string& path)
{
// ファイルを開く
// 拡張子確認
// JSON解析
// バイナリ解析
// メッシュ作成
// マテリアル作成
// テクスチャ作成
// ボーン作成
// アニメーション作成
// ShapeKey作成
// GPU転送
// エラー表示
}
巨大関数は、
どこで失敗したか分かりにくい。
どこで失敗したか分かりにくい。
良い分離例3
bool ModelImporter::Load(const std::string& path)
{
if (!OpenFile(path))
return false;
if (!ReadGeometry())
return false;
if (!ReadMaterials())
return false;
if (!ReadSkeleton())
return false;
if (!ReadAnimations())
return false;
return true; }
さらに分けてもよい。
bool ReadGeometry(); bool ReadMaterials(); bool ReadSkeleton(); bool ReadAnimations();
悪いコード例4 複数責務
悪い例:
void SaveModelAndPlaySoundAndShowDialog()
{
}
関数名を見ただけで、
複数の仕事を担当している。
複数の仕事を担当している。
良い例:
bool SaveModel();
void PlaySuccessSound();
void ShowSaveDialog();
必要なら上位関数で組み合わせる。
bool SaveProject()
{
if (!SaveModel())
return false;
PlaySuccessSound();
ShowSaveDialog();
return true; }
悪いコード例5 グローバル変数だらけ
悪い例:
Renderer* g_renderer; AudioEngine* g_audio; Model* g_model; bool g_running; int g_width; int g_height;
どこからでも変更できるため、
「誰が値を壊したのか」
が追いにくくなる。
改善例
class Application
{
private:
Renderer renderer;
AudioEngine audio;
bool running = true;
int width = 1280;
int height = 720;
};
必要な値を、
所有するクラスの内部へ置く。
所有するクラスの内部へ置く。
悪いコード例6 生new乱用
悪い例:
Model* model = new Model();
delete model;
delete忘れが起きる可能性がある。
途中でreturnすると、
Model* model = new Model();
if (error)
{
return;
}
delete model;
deleteへ到達しない。
改善例 RAII
auto model = std::make_unique<Model>();
スコープ終了時に自動解放される。
または、
値として持てる場合は、
値として持てる場合は、
Model model;
でよい。
所有権とは
C++では、
「このオブジェクトを誰が所有しているのか」
が非常に重要。
例えば、
class Scene
{
private:
std::vector<std::unique_ptr<Model>> models;
};
なら、
SceneがModelを所有している
という構造が比較的明確。
悪いコード例7 所有権不明
Model* model;
この1行だけでは、
- 誰が生成した?
- 誰がdeleteする?
- いつまで生きる?
- nullになる?
- 他の場所も所有している?
が分からない。
改善例
std::unique_ptr<Model> model;
単独所有ならunique_ptr。
共有所有が本当に必要なら、
std::shared_ptr<Model>
を使うこともある。
ただし、
何でもshared_ptrにすればよいわけではない。
何でもshared_ptrにすればよいわけではない。
寿命
C++では、
オブジェクトがいつ消えるかが重要。
オブジェクトがいつ消えるかが重要。
悪い例:
const std::string& GetName()
{
std::string name = "Model";
return name;
}
nameは関数終了時に破棄されるため、
その参照を返すのは危険。
その参照を返すのは危険。
改善例
std::string GetName()
{
return "Model";
}
悪いコード例8 範囲外アクセス
std::vector<int> values = {1, 2, 3};
int value = values[100];
存在しない場所へアクセスしている。
改善例
if (index < values.size())
{
int value = values[index];
}
または、
values.at(index);
を使用すると、
範囲チェックを行える。
範囲チェックを行える。
悪いコード例9 nullptr
Model* model = nullptr;
model->Render();
nullptrを参照している。
改善例
if (model)
{
model->Render();
}
悪いコード例10 空コード
AIコード生成などで特に注意。
bool LoadModel(const std::string& path)
{
return true;
}
一見、
「LoadModelが成功した」
ように見える。
しかし何も読み込んでいない。
これは、
コンパイルが通るだけのダミー実装。
空コードを見抜く
以下を確認。
- ファイルを本当に開いているか
- モデルデータを解析しているか
- 戻り値だけ返していないか
- TODOだけ残っていないか
- 仮データを返していないか
悪いコード例11 存在しないAPI
AIが生成した例:
auto model = VRMLoader::LoadModel(path);
しかし使用しているライブラリに、
VRMLoader
という型自体が存在しない場合がある。
コードの見た目が自然でも、
実APIとは限らない。
実APIとは限らない。
確認方法
- 公式ドキュメント
- ヘッダファイル
- IDE補完
- ライブラリソース
- コンパイル結果
を確認する。
悪いコード例12 エラー握り潰し
try
{
LoadModel();
}
catch (...)
{
}
何が失敗しても、
何も表示されない。
何も表示されない。
改善例
try
{
LoadModel();
}
catch (const std::exception& e)
{
Logger::Error(e.what());
}
さらに良い例
try
{
LoadModel();
}
catch (const std::exception& e)
{
Logger::Error(
std::string("Model loading failed: ") +
e.what()
);
return false; }
悪いコード例13 循環依存
A.h
#include "B.h"
class A
{
B b;
};
B.h
#include "A.h"
class B
{
A a;
};
AがBを要求し、
BがAを要求している。
BがAを要求している。
循環依存を減らす
前方宣言を利用できる場合がある。
A.h
#pragma once
class B;
class A
{
public:
void SetB(B* b);
private:
B* b = nullptr;
};
A.cpp
#include "A.h" #include "B.h"
void A::SetB(B* value)
{
b = value;
}
ただし、
完全型が必要な場所では
前方宣言だけでは足りない。
完全型が必要な場所では
前方宣言だけでは足りない。
依存関係の方向
良い設計では、
上位機能
↓
抽象化
↓
下位機能
↓
抽象化
↓
下位機能
という方向を意識する。
例:
Application ↓ Renderer ↓ GraphicsBackend ↓ OpenGL
Applicationが直接、
OpenGLの細かい関数
を大量に知る必要はない。
悪い例 OpenGL直結
class Application
{
public:
void Run()
{
glClear(...);
glBindVertexArray(...);
glUseProgram(...);
glDrawElements(...);
}
};
Applicationが描画APIへ強く依存している。
改善例
class Renderer
{
public:
void RenderScene();
};
Application側:
renderer.RenderScene();
OpenGL処理はRenderer内部へ置く。
インターフェース
C++では、
「何をできるか」
だけを外側へ見せ、
内部実装を隠す設計が重要。
内部実装を隠す設計が重要。
悪い例:
class Renderer
{
public:
GLuint vao;
GLuint vbo;
GLuint shader;
GLuint texture;
void Render(); };
外部からGPU内部状態を
何でも変更できてしまう。
何でも変更できてしまう。
改善例:
class Renderer
{
public:
bool Initialize();
void Render();
void Shutdown();
private:
GLuint vao = 0;
GLuint vbo = 0;
GLuint shader = 0;
};
カプセル化
privateを使い、
内部データを必要以上に公開しない。
内部データを必要以上に公開しない。
悪い例:
class Model
{
public:
std::vector<Vertex> vertices;
std::vector<unsigned int> indices;
};
どこからでも勝手に変更できる。
改善例:
class Model
{
public:
const std::vector<Vertex>& GetVertices() const;
void SetVertices(
std::vector<Vertex> vertices
);
private:
std::vector<Vertex> vertices;
};
const
変更しないものには
constを付ける。
constを付ける。
例:
void RenderModel(const Model& model);
この関数では、
Modelを書き換えないことが分かる。
Modelを書き換えないことが分かる。
悪い例 constなし
void PrintModel(Model& model);
表示するだけなのに、
書き換え可能になっている。
書き換え可能になっている。
関数の引数
巨大な値をコピーする必要がなければ、
const Model& model
のような参照を使うことがある。
命名
悪い例:
void DoThing(); void Process(); void Run2();
何をしているか分からない。
良い例:
bool LoadVRMFile();
void UpdateAnimation();
void RenderScene();
bool ValidateMesh();
boolだけでは情報不足な場合
bool LoadModel();
だけだと、
true
false
false
しか分からない。
改善例:
enum class LoadResult
{
Success,
FileNotFound,
InvalidFormat,
UnsupportedVersion,
ParseError
};
LoadResult LoadModel();
さらに詳細な結果型
struct ModelLoadResult
{
bool success = false;
std::string message;
std::unique_ptr<Model> model;
};
ログ
大型アプリではログが重要。
Logger::Info("Application started");
Logger::Warning("Texture missing");
Logger::Error("Failed to load model");
ログレベル
Debug Info Warning Error Fatal
などに分けるとよい。
3Dにおけるポール集中とC++
3Dでは、
1点へ大量のエッジが集中すると、
変形時に問題になることがある。
1点へ大量のエッジが集中すると、
変形時に問題になることがある。
C++でも、
1クラスへ大量の責務が集中
すると変更時に問題になる。
悪い構造:
Application ├─ Renderer ├─ Audio ├─ AI ├─ Model ├─ Texture ├─ Network ├─ Save ├─ Undo ├─ UI ├─ Search └─ Plugin
しかもApplication自身が
全処理を実装している。
全処理を実装している。
改善構造:
Application ├─ RenderSystem ├─ AudioSystem ├─ ModelSystem ├─ AISystem ├─ UndoSystem └─ UIManager
Applicationは、
これらの管理に集中する。
これらの管理に集中する。
3DにおけるエッジループとC++
エッジループは、
形状や変形に意味を持った流れを作る。
形状や変形に意味を持った流れを作る。
C++でも、
データがどのように流れるかを整理する。
データがどのように流れるかを整理する。
悪い例:
UI ↓ Model ↓ Audio ↓ Renderer ↓ UI ↓ FileManager ↓ Model
処理がぐちゃぐちゃ。
改善例:
UserInput ↓ Application ↓ Command ↓ Model ↓ Renderer
保存時:
Application ↓ ProjectSerializer ↓ FileSystem
イベントシステム
モジュールを直接接続しすぎないために、
イベントを使う場合もある。
イベントを使う場合もある。
例:
Window ↓ WindowResizeEvent ↓ Renderer
WindowがRenderer内部を
直接操作しなくてもよい。
直接操作しなくてもよい。
コマンドパターン
Undo/Redoが必要なアプリでは、
操作をCommandとして扱う方法が便利。
操作をCommandとして扱う方法が便利。
class Command
{
public:
virtual ~Command() = default;
virtual void Execute() = 0;
virtual void Undo() = 0;
};
例:
MoveObjectCommand AddObjectCommand DeleteObjectCommand
悪いUndo
if (action == 1)
{
}
if (action == 2)
{
}
if (action == 3)
{
}
操作が増えるほど巨大化する。
改善Undo
UndoManager ↓ Command ├─ MoveCommand ├─ DeleteCommand └─ RenameCommand
ファイル構造の攻略例
中規模アプリなら、
src/ ├─ main.cpp │ ├─ app/ │ ├─ Application.h │ └─ Application.cpp │ ├─ core/ │ ├─ Logger.h │ ├─ Logger.cpp │ ├─ Config.h │ ├─ Config.cpp │ ├─ FileSystem.h │ └─ FileSystem.cpp │ ├─ render/ │ ├─ Renderer.h │ ├─ Renderer.cpp │ ├─ Shader.h │ └─ Shader.cpp │ ├─ model/ │ ├─ Model.h │ ├─ Model.cpp │ ├─ Mesh.h │ ├─ Mesh.cpp │ ├─ ModelImporter.h │ └─ ModelImporter.cpp │ ├─ audio/ │ ├─ AudioEngine.h │ └─ AudioEngine.cpp │ ├─ undo/ │ ├─ Command.h │ ├─ UndoManager.h │ └─ UndoManager.cpp │ └─ ui/ ├─ MainUI.h └─ MainUI.cpp
main.cppの役割
理想的にはmain.cppは小さくする。
例:
#include "app/Application.h"
int main()
{
Application app;
if (!app.Initialize())
return 1;
app.Run();
app.Shutdown();
return 0; }
main.cppが、
5000行
になる必要はない。
Applicationの役割
Applicationは、
- 初期化
- メインループ
- 終了処理
などを担当。
例:
class Application
{
public:
bool Initialize();
void Run();
void Shutdown();
private:
Renderer renderer;
AudioEngine audio;
UIManager ui;
};
Rendererの役割
Rendererは描画だけ。
class Renderer
{
public:
bool Initialize();
void BeginFrame();
void RenderScene();
void EndFrame();
void Shutdown(); };
ModelImporterの役割
モデル読み込みだけ。
class ModelImporter
{
public:
virtual ~ModelImporter() = default;
virtual bool Load(
const std::string& path,
Model& model
) = 0;
};
派生:
FBXImporter
GLTFImporter
VRMImporter
ファイル形式を分離する
悪い例:
bool LoadModel(std::string path)
{
if (extension == "fbx")
{
// 500行
}
if (extension == "vrm")
{
// 700行
}
if (extension == "obj")
{
// 300行
}
}
改善:
ModelImporter ├─ FBXImporter ├─ VRMImporter └─ OBJImporter
依存関係
C++では外部ライブラリが増えるほど、
依存関係管理も重要になる。
依存関係管理も重要になる。
例:
Application ↓ Renderer ↓ GLFW ↓ OpenGL
モデル側:
ModelImporter ↓ Assimp
AI側:
LLMEngine ↓ llama.cpp
依存関係のバージョン
コードが正常でも、
- ライブラリバージョン違い
- DLL違い
- Debug/Release違い
- x86/x64違い
- Pythonバージョン違い
などで壊れる。
依存関係を固定する
例:
third_party/ ├─ glfw/ ├─ glm/ ├─ assimp/ ├─ pybind11/ └─ llama.cpp/
動作したバージョンや
Git commitを記録する。
Git commitを記録する。
dependencies.txt例
GLFW 3.4
Assimp 5.x
Python 3.12.x
llama.cpp commit XXXXXXXX
など。
CMake
CMakeは、
「ソースコード構造と依存関係を
ビルドシステムへ伝える」
ビルドシステムへ伝える」
役割を持つ。
3Dで例えるなら、
「モデルそのもの」
ではなく、
「どのパーツをどう組み立てるか」
を指定する制作設定に近い。
CMake基本構造
cmake_minimum_required(VERSION 3.20)
project(MyApplication
LANGUAGES CXX
)
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(MyApplication
src/main.cpp
src/app/Application.cpp
src/render/Renderer.cpp
)
target_include_directories(MyApplication PRIVATE
src
)
ライブラリ接続
target_link_libraries(MyApplication PRIVATE
glfw
OpenGL::GL
)
PRIVATE
そのターゲット自身だけで必要。
target_link_libraries(MyApplication PRIVATE
glfw
)
PUBLIC
そのターゲット自身と、
それを利用する側へ伝播させる。
それを利用する側へ伝播させる。
INTERFACE
そのターゲットを使う側だけへ
利用条件を伝える。
利用条件を伝える。
CMakeも構造化する
小さいうちは、
[[CMakeLists.txt]]
1個でもよい。
巨大化したら、
CMakeLists.txt
src/ ├─ CMakeLists.txt ├─ render/ │ └─ CMakeLists.txt ├─ model/ │ └─ CMakeLists.txt └─ audio/ └─ CMakeLists.txt
のように分離可能。
target単位で管理する
例:
add_library(RenderCore
src/render/Renderer.cpp
)
target_include_directories(RenderCore PUBLIC
src
)
target_link_libraries(RenderCore PRIVATE
OpenGL::GL
)
本体:
add_executable(MyApplication
src/main.cpp
)
target_link_libraries(MyApplication PRIVATE
RenderCore
)
なぜtarget単位がよいか
悪い例:
include_directories(...)
link_libraries(...)
のように、
ディレクトリ全体へ影響を広げると、
ディレクトリ全体へ影響を広げると、
「どのライブラリが何を必要としているか」
が分かりにくくなる。
target単位なら、
RendererはOpenGL
ImporterはAssimp
AIはllama.cpp
のように関係が読みやすい。
3Dの綺麗なトポロジーに相当するCMake
例:
Application ↓ RenderCore ↓ OpenGL
Application ↓ ModelCore ↓ Assimp
Application ↓ AudioCore
Application ↓ AICore ↓ llama.cpp
依存関係の流れが明確。
悪い依存構造
Application ↓ EverythingLibrary ↓ OpenGL ↓ Assimp ↓ Python ↓ CUDA ↓ Audio ↓ Network ↓ UI
どの機能も巨大EverythingLibraryへ依存する。
修正時の影響範囲が広い。
リファクタリング
3Dでいうリトポロジーに近い。
「動作や外部仕様をなるべく維持しながら、
コード内部の構造を整理する」
コード内部の構造を整理する」
こと。
例:
Before
class Application
{
// 4000行
};
After
Application ├─ Renderer ├─ AudioEngine ├─ ModelManager ├─ ProjectManager └─ UIManager
テスト
構造を分離すると、
機能単位のテストもしやすい。
機能単位のテストもしやすい。
tests/ ├─ [[test]]_model.cpp ├─ test_audio.cpp ├─ test_importer.cpp └─ test_math.cpp
悪いテスト構造
アプリ全体を起動しないと、
ModelImporter
単体をテストできない。
良いテスト構造
ModelImporter importer;
Model model;
bool success =
importer.Load("test.fbx", model);
個別確認できる。
デバッグ
良い構造では、
VRMだけ壊れた
↓
VRMImporterを見る
↓
VRMImporterを見る
音声だけ壊れた
↓
AudioEngineを見る
↓
AudioEngineを見る
描画だけ壊れた
↓
Rendererを見る
↓
Rendererを見る
となる。
悪い構造では、
何か壊れた
↓
main.cpp 8000行を見る
↓
main.cpp 8000行を見る
になる。
コンパイルエラー
例:
expected ';'
構文ミス。
リンクエラー
例:
unresolved external symbol
宣言は見えているが、
実装やライブラリが正しくリンクされていない可能性。
実装やライブラリが正しくリンクされていない可能性。
実行時クラッシュ
コンパイル・リンク成功後でも発生する。
例:
- nullptr
- use-after-free
- 範囲外アクセス
- 不正なDLL
- GPU API誤使用
- データ破損
など。
警告を無視しない
コンパイラ警告は、
潜在バグを見つける助けになる。
潜在バグを見つける助けになる。
例:
- 未使用変数
- 符号付き/符号なし比較
- 暗黙変換
- 戻り値無視
など。
コメント
悪い例:
// iを1増やす i++;
コードを読めば分かる。
良い例:
// Index 0 is reserved for the root node. ++index;
「なぜ」
を書く。
README
プロジェクトにREADMEを置く。
README.md
内容例:
- 何のプロジェクトか
- 必要な依存関係
- CMakeバージョン
- ビルド方法
- 対応OS
- 既知の問題
設計メモ
docs/ ├─ architecture.md ├─ dependencies.md └─ build.md
を置いてもよい。
初心者向け攻略順
- STEP1
main.cppだけ作る。
Hello World
を表示する。
- STEP2
Applicationクラスを作る。
main.cpp Application.h Application.cpp
- STEP3
Rendererを分離。
Renderer.h Renderer.cpp
- STEP4
モデル読み込みを分離。
ModelImporter
- STEP5
ログを追加。
Logger
- STEP6
Undoを追加。
UndoManager
- STEP7
外部ライブラリを1個ずつ追加。
- STEP8
CMakeターゲットを整理。
- STEP9
テストを追加。
- STEP10
巨大クラスができたら
リファクタリング。
リファクタリング。
攻略の鉄則
一気に全部作らない。
悪い攻略:
GLFW OpenGL Assimp Python CUDA AI VRM FBX Audio
を一気に追加。
エラー発生。
原因不明。
良い攻略:
GLFW追加 ↓ ビルド成功確認
OpenGL追加 ↓ ビルド成功確認
Assimp追加 ↓ ビルド成功確認
Python追加 ↓ ビルド成功確認
3D自動生成との共通点
3Dでは、
「Quadになった」
だけでは良いトポロジーとは言えない。
C++でも、
「コンパイルが通った」
だけでは良いコードとは言えない。
3D:
Quad ↓ でも関節で破綻
C++:
Compile Success ↓ でも機能追加で破綻
コード品質の判断
以下を確認。
- 役割は分かれているか
- 巨大クラスがないか
- 巨大関数がないか
- 循環依存していないか
- 所有権が分かるか
- 寿命が安全か
- エラーが記録されるか
- テスト可能か
- 依存関係が分かるか
- CMake構造が読みやすいか
悪いコードチェックリスト
以下が多い場合は要注意。
- main.cppが巨大
- 1クラス数千行
- 何でもpublic
- グローバル変数だらけ
- new/delete乱用
- raw pointerの所有権不明
- 循環include
- catch(...)で無視
- boolだけで全エラー処理
- 空実装
- TODOだらけ
- 存在しないAPI
- コピペコード大量
- 依存ライブラリのバージョン不明
- CMakeに謎パス直書き
- 全ターゲットへ同じライブラリをリンク
- 変更すると無関係な箇所まで壊れる
良いコードチェックリスト
- 役割ごとにファイル分割
- クラスの責務が明確
- main.cppが小さい
- 所有権が明確
- RAII
- constを適切に使う
- エラーを握り潰さない
- ログがある
- 機能単位でテスト可能
- 外部依存を分離
- CMakeをtarget単位で管理
- 依存関係を固定
- 変更範囲を限定できる
- 関数名だけで目的が分かる
重要
ファイルを分ければ自動的に良いコードになるわけではない。
悪い例:
FileA.cpp FileB.cpp FileC.cpp FileD.cpp
に分けたが、
全部が互いに依存している。
全部が互いに依存している。
これは、
「メッシュを別オブジェクトに分けただけで、
トポロジー自体は整理されていない」
トポロジー自体は整理されていない」
のと同じ。
本当に重要なこと
重要なのは、
「どこでファイルを分けたか」
ではなく、
「なぜそこで分けたか」
である。
Renderer.cppが存在する理由:
描画という責務を分離するため。
AudioEngine.cppが存在する理由:
音声という責務を分離するため。
ModelImporter.cppが存在する理由:
モデル読み込みという責務を分離するため。
最終的な考え方
3Dモデリングでは、
「このエッジはなぜここにあるのか」
を考える。
C++では、
「このクラスはなぜ存在するのか」
「この依存関係はなぜ必要なのか」
を考える。
意味のないエッジを増やさない。
意味のない依存関係を増やさない。
変形中心へポールを置かない。
変更が集中する場所へ依存関係を集中させない。
3Dでは、
「動かすことを考えたトポロジー」
を作る。
C++では、
「変更することを考えたコード構造」
を作る。
まとめ
C++における
「3Dトポロジー相当のもの」は、
「3Dトポロジー相当のもの」は、
単一の機能ではない。
主に、
- ファイル構造
- クラス構造
- 依存関係
- 所有権
- 寿命
- インターフェース
- CMakeターゲット構造
- データフロー
などを含めた
「ソフトウェアアーキテクチャ」
全体で考える。
3D:
良い形 + 良いトポロジー = 動かしやすいモデル
C++:
正しい処理 + 良い構造 = 保守しやすいソフト
つまり、
「動いたから完成」
ではない。
「変更しても壊れにくい」
「バグを追える」
「依存関係を理解できる」
「機能を追加できる」
「バグを追える」
「依存関係を理解できる」
「機能を追加できる」
ところまで設計されて、
初めて実用的なC++プロジェクトになる。
初めて実用的なC++プロジェクトになる。
一言で覚える
3D:
「エッジを適当に流すな」
C++:
「依存関係を適当に流すな」
これが、
C++におけるトポロジー攻略の基本である。
C++におけるトポロジー攻略の基本である。









