ダダダダダダダダダダダダダダダダダダダダダダダダダダダダダダイヤモンド継承
ジョジョ
私はジョジョだと第四部が一番好きです。
最終回かその一個前のopがカッコ良すぎて痺れます。
是非観てない人は観てみてください(゜ロ゜
目次
1. 継承とは何か
継承は既存のクラス(基底クラスまたは親クラスと呼ばれる)の特性を新しいクラス(派生クラスまたは子クラスと呼ばれる)に引き継ぐプロセスを指します。
このメカニズムにより、派生クラスは基底クラスのメソッドや変数を「継承」し、既存のコードを再利用しつつ、新たな機能を追加または既存の機能を変更(オーバーライド)することができます。
コード例
#include <iostream>
class Base {
public:
virtual void print() {
std::cout << "Base class" << std::endl;
}
};
// 継承クラス・子クラス
class Level1 : public Base {
public:
void print() override {
std::cout << "Level 1" << std::endl;
}
};
int main() {
Level1 obj;
obj.print(); // "Level 1"が出力されることを期待
return 0;
}
やっぱり上限知りたい
プログラムを設計する過程で、継承を利用する際に「どれだけの継承が可能なのか?」という疑問が生じました。
特に、複数のクラスが同一の基底クラスから派生している場合、継承のチェーンがどの程度まで拡張できるのか、またその際に直面する問題や限界は何かという点に興味を持ちました。
さらに、継承の複雑さが増すと出くわす可能性のある「ダイヤモンド継承問題」にも注目しました。
ダイヤモンド継承(菱形継承)問題
これは、2つ以上の派生クラスが同一の基底クラスを共有し、その基底クラスのメンバが複数回継承されることで生じる問題です。
この問題への対処法としてvirtual
継承がどのように機能するのか、実際に検証してみることにしました。
2. 継承の上限を調べる
継承チェーンの長さには理論的な限界があるのか、また実際のコンパイルプロセスでどのような限界に遭遇するのかを調査することにしました。
この疑問を解明するために、私はPythonスクリプトを使用して、継承チェーンが異なる長さのC++クラスを動的に生成する実験を行いました。
実験の設定
まず、Baseクラスとその派生クラスLevel1を定義しました。次に、Level1クラスから継承し、それぞれが前のレベルから継承する形で、継承チェーンを形成する多数のクラスを生成しました。
このプロセスを自動化するためにPythonスクリプトを使用し、num_levels変数で指定された数だけクラスを生成しました。
コード例
num_levels = 100 # 生成したいレベルの数
# ヘッダー部分
code = """#include <iostream>
class Base {
public:
virtual void print() {
std::cout << "Base class" << std::endl;
}
};
class Level1 : public Base {
public:
void print() override {
std::cout << "Level 1" << std::endl;
}
};
"""
# 各レベルのクラスを生成
for i in range(2, num_levels + 1):
code += f"""
class Level{i} : public Level{"" if i == 1 else i-1} {{
public:
void print() override {{
std::cout << "Level {i}" << std::endl;
}}
}};
"""
# main関数
code += """
int main() {
""" + f"""
Level{num_levels} obj;
obj.print(); // "Level {num_levels}"が出力されることを期待
""" + """
return 0;
}
"""
# 生成したコードをファイルに書き出す
with open("generated.cpp", "w") as f:
f.write(code)
そしてこれを実行します。
python3 generate_cpp.py
そうするとgenerated.cpp
が出来ます。
#include <iostream>
class Base {
public:
virtual void print() {
std::cout << "Base class" << std::endl;
}
};
class Level1 : public Base {
public:
void print() override {
std::cout << "Level 1" << std::endl;
}
};
class Level2 : public Level1 {
public:
void print() override {
std::cout << "Level 2" << std::endl;
}
};
......
......
class Level100 : public Level100 {
public:
void print() override {
std::cout << "Level 100" << std::endl;
}
};
int main() {
Level100 obj;
obj.print(); // "Level 100"が出力されることを期待
return 0;
}
このnum_levels
を増やしていこう、ということです。
実験結果
このスクリプトを使用して、1987
レベルまでのクラスを正常に生成し、コンパイルすることができました。
これは、私の環境下での安全な継承チェーンの上限を示しています。(環境によって変わる可能性大)
しかし、num_levels
を1988
に増やしたとき、コンパイラはIllegal instruction: 4
エラーを報告し、コンパイルプロセスがクラッシュしました。
clang: error: unable to execute command: Illegal instruction: 4
clang: error: clang frontend command failed due to signal (use -v to see invocation)
Apple clang version 15.0.0 (clang-1500.1.0.2.5)
Target: arm64-apple-darwin23.0.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin
clang: note: diagnostic msg:
********************
PLEASE ATTACH THE FOLLOWING FILES TO THE BUG REPORT:
Preprocessed source(s) and associated run script(s) are located at:
clang: note: diagnostic msg: /var/folders/ww/k7qvgq4x7hqf75_sd27z4wyr0000gn/T/generated-f3935d.cpp
clang: note: diagnostic msg: /var/folders/ww/k7qvgq4x7hqf75_sd27z4wyr0000gn/T/generated-f3935d.sh
clang: note: diagnostic msg: Crash backtrace is located in
clang: note: diagnostic msg: /Users/horikekaishuu/Library/Logs/DiagnosticReports/clang_<YYYY-MM-DD-HHMMSS>_<hostname>.crash
clang: note: diagnostic msg: (choose the .crash file that corresponds to your crash)
clang: note: diagnostic msg:
この結果は、私の開発環境における継承チェーンの実用的な上限が1987
レベルであることを示しています。
エラーメッセージの内容
Illegal instruction
Illegal instruction
は、プログラムが実行しようとした命令が無効、または実行不可能であることを意味します。
この文脈では、clang
コンパイラがソースコードのコンパイル中に、サポートされていない、または予期せぬ命令に遭遇したことを指します。
clang: error: unable to execute command: Illegal instruction: 4
この行は、clang
がコマンドを実行できなかったこと、そしてその理由がIllegal instruction
であることを示しています。
コンパイルプロセス中に何らかの問題が発生し、正常に進行できなかったことを意味します。
Apple clang version 15.0.0...
ここでは、問題が発生したclang
コンパイラのバージョンと、それが実行されたオペレーティングシステムの情報を提供しています。
PLEASE ATTACH THE FOLLOWING FILES TO THE BUG REPORT:
このセクションは、エラー報告を行う際に添付すべきファイルについての指示を提供しています。
コンパイラが生成したプリプロセスされたソースファイルや、実行スクリプト、クラッシュのバックトレースが含まれるファイルのパスが示されています。
これらのファイルは、エラーの原因を特定する上で役立ちます。
Crash backtrace is located in
このメッセージは、クラッシュ時のバックトレースが記録されたファイルの場所を指示しています。
バックトレースは、プログラムがクラッシュした時点での関数呼び出しの履歴を示し、デバッグに非常に有用です。
まとめ
Illegal instruction: 4
エラーはコンパイラが無効な命令に遭遇したことを示しており、コンパイルプロセスが予期せず終了したことを意味します。
つまり私の環境ではここが限界値です。(この例だと)
num_levels = 1987
セーフ
num_levels = 1988
アウト
3. ダイヤモンド継承の限界を調べる
実験設定
先ほど同様Pythonスクリプトを使用して、Baseクラスを基底とし、そこからvirtual
に継承する多数の中間クラスを生成し、最終的にそれら全てを継承するFinalClass
クラスを作成するプログラムを自動生成する実験を行いました。
コード例
num_classes = 100# 生成したい中間クラスの数
# ヘッダー部分と基底クラスの定義
code = """#include <iostream>
class Base {
public:
virtual void print() {
std::cout << "Base class" << std::endl;
}
};
"""
# 中間クラスの生成
for i in range(1, num_classes + 1):
code += f"""
class IntermediateClass{i} : virtual public Base {{
public:
void print() override {{
std::cout << "IntermediateClass{i}" << std::endl;
}}
}};
"""
# FinalClassクラスの定義
code += "class FinalClass :"
for i in range(1, num_classes + 1):
if i < num_classes:
code += f" public IntermediateClass{i},"
else:
code += f" public IntermediateClass{i} {{"
code += f"""
public:
void print() override {{
std::cout << "FinalClass class, derived from " << {num_classes} << " intermediate classes." << std::endl;
}}
}};
"""
# main関数とその他の部分は変更なし
# main関数
code += """
int main() {
FinalClass obj;
obj.print(); // "FinalClass class"が出力されることを期待
return 0;
}
"""
# 生成したコードをファイルに書き出す
with open("generated_diamond.cpp", "w") as f:
f.write(code)
そしてこれを実行します。
python3 generate_cpp.py
そうするとgenerated_diamond.cpp
が出来ます。
#include <iostream>
class Base {
public:
virtual void print() {
std::cout << "Base class" << std::endl;
}
};
class IntermediateClass1 : virtual public Base {
public:
void print() override {
std::cout << "IntermediateClass1" << std::endl;
}
};
......
......
class IntermediateClass100: virtual public Base {
public:
void print() override {
std::cout << "IntermediateClass100" << std::endl;
}
};
class FinalClass : public IntermediateClass1, IntermediateClass2, ..., IntermediateClass100
public:
void print() override {
std::cout << "FinalClass class, derived from " << 100000 << " intermediate classes." << std::endl;
}
};
int main() {
FinalClass obj;
obj.print();
return 0;
}
先程同様このnum_classes
を増やしていこう、ということです。
実験結果
意外なことに、num_classes
を100万個に増やした複雑なダイヤモンド継承構造でも、C++のコンパイラは時間は物凄くかかりましたが(10時間くらい)問題なくコードをコンパイルし、プログラムは正常に動作しました。
これはC++のコンパイラとランタイム環境が非常に高度に最適化されていることを示しています。
virtual
継承を通じて、C++は基底クラスのインスタンスを効率的に管理し、複数の継承が引き起こす問題を解決する能力を持っています。
vtable
この結果はvirtual
継承により生成されるvtable
の効率的な管理によって可能となります。
vtable
は、仮想関数の呼び出しを正確に管理し、多重継承における曖昧さを解消する核心技術です。
vtable
(仮想関数テーブル)によって、「索引」や「目次」が提供されることで、プログラムは必要な関数(メソッド)を迅速に特定し、呼び出すことができます。
これにより、多数のクラスとメソッドがあっても、効率的な検索とアクセスが可能になり、システムのパフォーマンスが向上します。
従来の方法では、各関数への直接的な参照や羅列に頼っていたため、関数の数が増えるほど検索時間が長くなり、効率が落ちていました。
しかし、vtable
を使うことで、実行時に動的に適切な関数を選択する「目次」が作成されるため、関数の数が増えても検索時間が大幅に短縮され、効率的な実行が可能になります。
つまり、
「vtableにより、目次が付いた辞書のようになり、関数の検索が格段に迅速かつ効率的になった」
ということになります。
結論
ダイヤモンドは砕けない。
4. おわりに
まあ、でも数を増やせばいつかはダイヤモンドも砕けると思います。(やりませんが)
是非やりたい方はご自身の環境で試してみてください。(安全保証はないです)
途中、パソコンが悲鳴をあげていたので、不安になりましたが、新品だし大丈夫だろう、という謎の自信の元、決行しました。
ごめんね、ノーパソ。
お前が一番ダイヤモンドだったよ。