アドベントカレンダー2021 23日目の記事です。
こんにちは、@grkonです。記事を読んでくれてありがとうございます。この記事では僕がC++で出た謎のエラー「Undefined symbols for architecture arm64」と戦った話を書こうと思います。自分の行った解決法だけしか書けませんが、最後の方に書いてあるので、ぜひ悩まされている方は参考にしてください。
というか、記事の題名長すぎたかもしれないですね。
経緯
僕はC++を愛用(笑)してきましたが、最近人生で初めて分割コンパイルに挑戦してみました。(今まではヘッダーに全部記述していました。)cmakeなどを使っていろいろやってました。それまではヘッダーと実装のプログラムに手をかけていたのですが、ようやくmain関数に手を出しました。
main関数で、ヘッダで定義した関数を呼び出したら、なんと謎のエラー「Undefined symbols for architecture arm64」が出たではありませんか!!!
そして、ここから2時間ほどの格闘が始まるわけです...。
Undefined symbols for architecture arm64
なにこれ。
英文で言いたい事は何となく察しました。多分「そんな関数ないよ」ってことですよね。ん?なぜ?ヘッダでは定義したし、他の関数で試したらエラー出ないんですよね。
そこで色々調べました。
検索結果がclangの特性の話ばかり
検索してもほとんどこの記事しか出てこないので、これを信じてほとんど格闘しました。
まずclangって何?という話ですが、これはC/C++のコンパイラです。広く使われているのは gcc というコマンド群ですが、Mac OSとかを使うと、なぜかclangが優先されています。
そして本題ですが、調べたところ、このエラーはclang特有のエラーだという話が多くありました。どうも、clangでC++をコンパイルすると、リンクだっけ?ライブラリだっけ?を読み込めなくてエラーが出るということが多々あるらしいです。
なるほど。
そして、いろんなファイルを探ると、cmakeはclangをデフォルトで使っていたことが判明しました。link.txtってファイルを見ると一番わかりやすいです。
なるほどなるほど。
つまり、gccに変更してやればいいわけだな、と思いまして、ここでcmakeの設定をちょいといじりました。具体的には、CMakeLists.txt
CMakeLists.txtset(CMAKE_C_COMPILER "gcc")
set(CMAKE_CXX_COMPILER "g++")
という文字列を追加しました。これは、英語に書いてある通り、cmakeのコンパイラにgccを使ってね、という命令文です。コンピュータは命令したら何でも聞いてくれるメイドさんなので、言う事を聞いてくれました。そしたらなんということでしょう。
Undefined symbols for architecture arm64:
何故だ!!!
しかし、進展がありました!なんと、この後に続くエラーの文が少々変わってる!gcc仕様になった文が吐き出されていました。
つまり、コンパイラのせいではなかったということですね!長い時間がかかりましたが、これは素晴らしい進歩ですよ。(つまり、gccでも出るエラーということですね。検索結果に踊らされていました)
見つけ出す!解決策と原因!
そして、検索に検索を重ね、やっと見つけたのです。
原因はそんな他人のせいにできるような場所にはありませんでした。
なんとtemplateの仕様が原因だったのです!!
どういうコードを組んだか、簡単にお教えします。分割コンパイルなので、ヘッダーで宣言した関数をC++のファイルで実装するという流れを取りました。こいつが原因の関数です。
test.hpp/*ヘッダー*/
template <typename T> bool data_init(int, bool);
test.cpp/*C++ソース*/
template <typename T> bool data_init(int id, bool _delete) {
/*処理*/
}
ハテ、何が問題でしょうか?と思ったそこのあなた!僕も同じこと思いました。
実は...
templateを明示的に宣言していないのが原因だったのです!!!
どういうことですか、という話ですが、実は分割コンパイルでは、templateでは各templateの場合について実装を列挙しなければならなかったのです。つまり、例えばTがdoubleの時の処理をさせたいときは
test.cpptemplate <> bool data_init<double>data_init(int id, bool _delete) {
/*処理*/
}
と書く必要があるのです。そしたらあら不思議!エラーが直るではありませんか!つまり、次のようなことが言えるんです。
分割コンパイルはtemplateで使いたい型名などで個別に用意しておく必要があるんです。でも、これっていちいち列挙して書いているようじゃせっかくのtemplateが台無しですよね。そんなわけで次のような解決策があります。
template は生きている!!
実は最初のコードで書いたような、型ごとに定義を明示しないtemplateは、同じファイル内では使えるんです!!もうあとはお分かりですね...?
こうするんです!!
test.cpptemplate <typename T> bool _data_init(int id, bool _delete) {
/*処理*/
}
template <> bool data_init<double>data_init(int id, bool _delete) {
return _data_init<double>(id, delete);
}
よくある手法ですね。一般化した別の関数(ここでは_data_init)を用意することで、列挙はするが同じ処理を何回も書くということは無くなるわけですね。
エラー完全討伐!嬉しい!
終わりに
以上で解決までの物語は終わりです!
少しでも皆さんの力になれていたらいいなと思います!
明日はmeraさんの記事です!お楽しみに!