ファームウェアエンジニアの中林です。たまにはコンパイラの謎挙動に苦しんだ一幕を取り上げるのも一興、ということで昨日出会ったコンパイルエラーの紹介です。
多分この issue です*1。 助けて詳しい人!
事の発端
何度か当ブログでも紹介している通り、C言語で書かれた Nature Remo ファームウェアのユニットテストは C++ で書いています。 基本的には C++ の高機能なので C でテストを書くより快適です。
一方で C では当然のようにできるけど、C++ ではできないこともあります。 その1つが指示付き初期化です (C++20 から一部仕様に入りました) 。
指示付き初期化
cpprefjp から引用します。
C++20では、波カッコによる集成体初期化においてメンバ名を指定して初期化が行える。
struct Point3D { int x; int y; int z = 0; }; struct Rect { Point3D p1; Point3D p2; }; // 以下の例では、変数名と初期化子リストの間に=を書いても良い Point3D p1 {1, 2, 3}; // (1) OK これは通常の集成体初期化 Point3D p2 {.x = 1, .y = 2, .z = 3}; // (2) OK (1)と同じ Point3D p3 {.x{1}, .y{2}, .z{3}}; // (3) OK (1)と同じ
Nature Remo の開発ではできるだけターゲットデバイス上でも動かせるように、今のところは C++17 でテストを書くようにしています。 とは言え、C++17 以前でも GNU 拡張で指示付き初期化が使えるため、テストを書く際は指示付き初期化をちょいちょい利用していました。
ジェネリックラムダ
また C++14 で使えるようになった便利な機能としてジェネリックラムダがあります。
ラムダを書く時に [](auto, auto) { hogehoge }
のようにパラメータに auto
を指定することができます。
Nature Remo のテストでは次のようにシグネチャの決まった std::function
に格納する関数オブジェクトを生成するときに便利に使っていました。
std::function<void(uint16_t, uint8_t)> f = [](auto, auto) { fugafuga };
事の発端 (真)
ある日、ルンルン気分でこれまでテストを書いていなかったコンポーネントにテストを書いてやるぞー、ということでテストを書き始めました。 そのコンポーネントで使っているライブラリの簡易モックを自作して、一通り手元の g++ でビルド & テストが通るようになりました。
そこで、GitHub Actions でワークフローを設定して、これから永遠に自動テストとして働いてもらうことにしました。 Nature Remo の自動テストではコードの移植性を高めたり、潜在バグを発見するために、g++ (gcc) / clang++ (clang) 両方でユニットテストを回しています。 そこで事件が発生します。
「なんか clang++ だけテストコードのコンパイルに失敗するんやけど…」
まあ大変!
最小の再現コードを作ってみる
ということで、色々試しながら作成した再現コードは次の通りです。
#include <cstdint> #include <functional> struct tagged_union { uint8_t type; union { struct { int value; } variant; }; }; int main(void) { std::function<void(uint8_t)> f = [](auto) { struct tagged_union v { .type = 0, .variant = { .value = 1 } }; return; }; return 0; }
C 言語でよくある union を使ったタグ付き共用体の初期化を、ジェネリックラムダ内で指示付き初期化するとコンパイルエラーになることがわかりました。 見ていただければわかるように、ジェネリックラムダのパラメータとはなんの関係もありません。 しかも、ジェネリックラムダの外であれば、この指示付き初期化はふっつーにコンパイルできます。
struct tagged_union v { .type = 0, .variant = { .value = 1 } };
ちなみにエラーはこのような感じです。 はい、わけがわかりませんね。
error: field designator (null) does not refer to any field in type 'struct tagged_union' /opt/wandbox/clang-head/include/c++/v1/__functional/invoke.h:391:28: note: in instantiation of function template specialization 'main()::(anonymous class)::operator()<unsigned char>' requested here _LIBCPP_CONSTEXPR decltype(std::declval<_Fp>()(std::declval<_Args>()...)) ^ /opt/wandbox/clang-head/include/c++/v1/__functional/invoke.h:401:19: note: while substituting deduced template arguments into function template '__invoke' [with _Fp = (lambda at prog.cc:16:9) &, _Args = <unsigned char>] static decltype(std::__invoke(std::declval<_XFp>(), std::declval<_XArgs>()...)) __try_call(int); ^ /opt/wandbox/clang-head/include/c++/v1/__functional/invoke.h:407:28: note: while substituting deduced template arguments into function template '__try_call' [with _XFp = (lambda at prog.cc:16:9) &, _XArgs = (no value)] using _Result = decltype(__try_call<_Fp, _Args...>(0)); ^ /opt/wandbox/clang-head/include/c++/v1/__type_traits/conjunction.h:27:32: note: in instantiation of template class 'std::__invokable_r<void, (lambda at prog.cc:16:9) &, unsigned char>' requested here __expand_to_true<__enable_if_t<_Pred::value>...> __and_helper(int); ^ /opt/wandbox/clang-head/include/c++/v1/__type_traits/conjunction.h:38:39: note: while substituting explicitly-specified template arguments into function template '__and_helper' using _And _LIBCPP_NODEBUG = decltype(std::__and_helper<_Pred...>(0)); ^ /opt/wandbox/clang-head/include/c++/v1/__functional/function.h:978:33: note: in instantiation of template type alias '_And' requested here template <class _Fp, bool = _And< ^ /opt/wandbox/clang-head/include/c++/v1/__functional/function.h:997:54: note: in instantiation of default argument for '__callable<(lambda at prog.cc:16:9) &>' required here using _EnableIfLValueCallable = typename enable_if<__callable<_Fp&>::value>::type; ^~~~~~~~~~~~~~~~ /opt/wandbox/clang-head/include/c++/v1/__functional/function.h:1008:33: note: in instantiation of template type alias '_EnableIfLValueCallable' requested here template<class _Fp, class = _EnableIfLValueCallable<_Fp>> ^ /opt/wandbox/clang-head/include/c++/v1/__functional/function.h:1009:5: note: in instantiation of default argument for 'function<(lambda at prog.cc:16:9)>' required here function(_Fp); ^~~~~~~~~~~~~ prog.cc:15:34: note: while substituting deduced template arguments into function template 'function' [with _Fp = (lambda at prog.cc:16:9), $1 = (no value)] std::function<void(uint8_t)> f = ^ 1 warning and 1 error generated.
そして冒頭 issue へ
再現方法がわかったので、clang に issue が立ってないか調べました。 最初は generic lambda の推論周りかと思って探していたのですが、該当しそうな issue が見つかりませんでした。
ふと思い立って、anonymous union で GitHub を検索したところ、冒頭の issue に辿りつくことが出来ました。
コンパイルエラーを回避するには?
既知の問題であることはわかったので、今度は自分のプログラムをどうするか、です。 ワークアラウンドとしては2つ考えられます。
- generic lambda にしない
- union 部分だけ初期化を飛ばして、再代入する
1 のワークアラウンドはこうです。template parameter が絡まなければ良さそうなので、まあそうですね。
std::function<void(uint8_t)> f = [](uint8_t) { struct tagged_union v { .type = 0 }; v.variant = { .value = 1 }; return; };
ただ、ラムダに渡すパラメータ数が多いといちいち型を書くのがめんどうなので、嫌です。何のためにジェネリックラムダがあると思っているのですか?弁償して下さい。という気持ちになってします。
- は、まあ不格好ですけど個人的には許容範囲内なかぁ、ということで、こちらを使うことにします。つまり、無名共用体の初期化を飛ばす感じでこうです。
std::function<void(uint8_t)> f = [](auto) { struct tagged_union v { .type = 0 }; v.variant = { .value = 1 }; return; };
ださいですけど、コードの記述量としても対して変わらないので、まあ良しとするか…、という感じです。
エンジニア積極採用中です
Natureではコンパイラの謎挙動に遭遇しても泣かない仲間を募集しています。
カジュアル面談も歓迎なので、ぜひお申し込みください。
Natureのミッション、サービス、組織や文化、福利厚生についてご興味のある方は、ぜひCulture Deckをご覧ください。
*1:本記事で遭遇したのは無名共用体でしたが、多分同じなのでしょう