要約
- 部分特殊化したテンプレートをネストした場合、msvcで完全型として展開されないケースがある
- 継承を使って回避することはできるが、気をつけよう
コンパイラの仕様のコーナーケースを突くような話になってきました。
事例
template<typename F>
class Func;
template<typename TResult, typename ...TArgs>
class Func<TResult(TArgs...)> {};
template<typename T, size_t TAlign = alignof(Func<void(T)>)>
class FuncOwner {};
template<typename T>
class Hoge {
FuncOwner<T> owner;
};
- 部分特殊化を使った関数オブジェクトストレージを定義する(参考)
- 任意の型を引数に取り、それを1.の型に対する引数の一部(返値でも引数でも可)で利用した上で、完全型でないと不可能な評価をデフォルト引数上で行う(alignofやsizeofなど)テンプレートクラスを定義する
- 2.を利用するテンプレートクラスを定義する
これが、msvc(15.7.5)では error C2027: 認識できない型 'Func<void(T)>' が使われています。
となります。
gcc(9.0.0)やclang(8.0.0)ではエラーにはなりませんでした。
解決策
部分特殊化を行った型を直接利用せず、継承を挟むことで回避できます。
template<typename F>
class FuncImpl;
template<typename TResult, typename ...TArgs>
class FuncImpl<TResult(TArgs...)> {};
template<typename F>
class Func : public FuncImpl<F> {};
恐らくですが、部分特殊化のマッチングを解決するタイミングが、テンプレートパラメータのデフォルト引数上ではなく、親クラスの指定時にずれることで、問題が解消したものと思われます。
分析
テンプレートの展開は、具体的な型が与えられてから行うものだと思っていましたが、msvcは違うようです。
-
FuncOwner
を利用するHoge
が定義された時点で、ある程度の展開を行おうとする - デフォルト引数に含まれる
Func<void(T)>
のTが与えられていないことで、定義済みの部分特殊化のマッチングから外れる - 宣言のみの
Func<F>
を利用しようとして、定義がないためにエラーになる
といった感じでしょうか。
実際に、Hoge
の定義を削除してFuncOwner
を直接具体的な型で利用するコードを記述した場合は、エラーになりません。
また、FuncOwner
のアライメントの値をクラススコープ内の定数として定義した場合もエラーになりません。
前者はテンプレートのネストがトリガーになっていること、後者は型引数リスト部とクラススコープ内(そして恐らく親クラスの指定時も)で評価のタイミングが異なることを示唆していると考えられます。
考察
- 結局msvcが間違ってるんだろうか
- 型引数のデフォルト値で完全型を期待するのも良くない気はするが、型引数でサイズを指定するニーズは無くせない(と思う)
- とりあえず部分特殊化したら、継承で包んでおくのが無難?