首先解释一下这个看起来很奇怪的标题。
“带默认模板参数的类模板”是指 Class template with default template-parameter,即一个类模板的部分或全部模板参数有默认值,比如:
template<typename T1, typename T2 = int>
struct Foo { };
有个很典型的例子是 std::vector
,它其实有两个模板参数:
template<class T, class Allocator = std::allocator<T>>
class vector;
只不过绝大多数情况下,我们并不需要指定第二个模板参数,感觉上好像它就只有一个模板参数,比如:std::vector<int> vec;
。
“模板模板参数”即 Template template parameter,也就是带模板的模板参数,比如:
template <template <typename> class C, typename T>
void bar(const C<T>& c) { }
这里面的 C
就是一个模板模板参数。
现在,问题来了,在已有上述类 Foo
和函数 bar()
定义的情况下,下面这个代码能够通过编译吗?
bar(Foo<int>{});
这个问题其实是在问,template<typename T1, typename T2 = int>
能不能与 template <typename> class C
进行匹配。
一方面,C
这个类模板只能有一个模板参数,而 Foo
是有两个模板参数的,所以看起来两者应该无法匹配。但另一方面,Foo
第二个模板参数有默认值,它在实际使用中完全可以看作只有一个模板参数,所以似乎可以匹配 C
。
该问题答案是:取决于编译器!
参见 cppreference,C++17 引入了一个新的特性 Matching template template parameters to compatible arguments (模板模板形参与可兼容实参匹配,貌似没有更简单的名字,暂且叫它 MTTP2CA 吧) 来解决 CWG 150 问题,使得以下代码能够编译:
template<class T> class A { /* ... */ };
template<class T, class U = T> class B { /* ... */ };
template <class ...Types> class C { /* ... */ };
template<template<class> class P> class X { /* ... */ };
X<A> xa; // OK
X<B> xb; // OK in C++17 after CWG 150
// Error earlier: not an exact match
X<C> xc; // OK in C++17 after CWG 150
// Error earlier: not an exact match
所以,不支持 C++17 的编译器是不能编译 bar(Foo<int>{});
的。但问题并不仅限于此,即使是目前最新的 Clang 12 和 MSVC v19,也都不行,只有 GCC 能够编译这样的代码,这是为什么呢?
原因是 MTTP2CA 特性其实有一个缺陷,假设上述 bar()
函数新增一个定义:
template <template <typename, typename> class C, typename T1, typename T2>
void bar(const C<T1, T2>& c) { }
此时 bar(Foo<int>{});
匹配的是 void bar(const C<T>& c);
还是 void bar(const C<T1, T2>& c);
呢?
类似的,如果加入一个新的类模板:
template <class>
struct Bar;
template <template <class> class X, class T>
struct Bar<X<T>> { };
template <template <class, class> class X, class T, class U>
struct Bar<X<T, U>> {
};
那么 Bar<Foo<int>> a;
匹配的是 Bar<X<T>>
还是 Bar<X<T, U>>
呢?
事实上这两种情况在 MTTP2CA 中是没有定义的,也就是说 C++ 标准并没有规定此时应该匹配前者(一个模板形参)还是后者(两个模板形参)。我的测试结果是 GCC 会选择匹配后者。
因为这个未定义的情况,Clang 和 MSVC 在默认情况下隐藏了这一特性。Clang 可以通过 -frelaxed-template-template-args
编译选项来打开该特性,MSVC 我还没找到开启方法。
GCC 和 Clang 在该问题上的不一致还带来了一个比较神奇的现象:如果你 IDE 用的是 QtCreator,并开启了 ClangCodeModel 插件(自动分析代码问题,高版本 QtCreator 默认开启),那么在编译前面提到的代码时,QtCreator 会报错(ClangCodeModel 基于 Clang 对代码进行分析),但同时又能够编译成功(Linux 上的 QtCreator 默认使用 GCC 作为编译器)。