熟悉模板编程的读者应该知道,相对于以类型为模板参数的模板而言,以非类型为模板参数的模板实例化规则更加严格。在C++17标准之前,这些规则包括以下几种。
1.如果整型作为模板实参,则必须是模板形参类型的经转换常量表达式。所谓经转换常量表达式是指隐式转换到某类型的常量表达式,特点是隐式转换和常量表达式。这一点很好理解,例如:
constexpr char v = 42;
constexpr char foo() { return 42; }
template<int> struct X {};
int main()
{
X<v> x1;
X<foo()> x2;
}在上面的代码中constexpr char到int的转换就满足隐式转换和常量表达式。
2.如果对象指针作为模板实参,则必须是静态或者是有内部或者外部链接的完整对象。
3.如果函数指针作为模板实参,则必须是有链接的函数指针。
4.如果左值引用的形参作为模板实参,则必须也是有内部或者外部链接的。
5.而对于成员指针作为模板实参的情况,必须是静态成员。
请注意,以上提到的后4条规则都强调了两种特性:链接和静态。因为一旦代码满足了这些要求,就表明实参指引的内存地址固定了下来,对于编译器而言这是实例化模板的关键。比如:
template<const char *> struct Y {};
extern const char str1[] = "hello world"; // 外部链接
const char str2[] = "hello world"; // 内部链接
int main()
{
Y<str1> y1;
Y<str2> y2;
}除了上面的规则以外,其他的实例化方式都是非法的,这其中也包括了一些合理场景,例如返回指针的常量表达式:
int v = 42;
constexpr int* foo() { return &v; }
template<const int*> struct X {};
int main()
{
X<foo()> x;
}上面的代码在C++17之前是无法编译成功的,因为模板并不接受foo()的返回值类型,根据第一条规则它只会接受整型的经转换常量表达式。
在C++17标准中,C++委员会对这套规则做了重新的梳理,一方面简化规则的描述,另一方面也允许常量求值作为所有非类型模板的实参。新的标准只强调了一条规则:非类型模板形参使用的实参可以是该模板形参类型的任何经转换常量表达式。其中经转换常量表达式的定义添加了对象、数组、函数等到指针的转换。这从另一个角度对以前的规则进行了兼容。
在新规则的支持下,上面的代码可以编译成功,因为新规则不再强调经转换常量表达式的求值结果为整型。由于规则的修改,还带来了一个有趣的变化。仔细观察新规则会发现,现在对于指针不再要求是具有链接的,取而代之的是必须满足经转换常量表达式求值。这就是说,下面的代码可以顺利地编译通过:
template<const char *> struct Y {};
int main()
{
static const char str[] = "hello world";
Y<str> y;
}在C++17以前,上面的代码会编译失败,给出的错误提示为&str,而不是一个有效的模板实参,因为str没有链接。不过C++17不存在上述问题,代码能够顺利地编译通过。
最后要强调的是,新规则并非万能,以下对象作为非类型模板实参依旧会造成编译器报错。
1.对象的非静态成员对象。
2.临时对象。
3.字符串字面量。
4.typeid的结果。
5.预定义变量。
在C++11标准之前,将局部或匿名类型作为模板实参是不被允许的,但是这个限制并没有什么道理,所以在C++11标准中允许了这样的行为,让我们看一个提案文档中的例子:
template <class T> class X { };
template <class T> void f(T t) { }
struct {} unnamed_obj;
int main()
{
struct A { };
enum { e1 };
typedef struct {} B;
B b;
X<A> x1; // C++11编译成功,C++03编译失败
X<A*> x2; // C++11编译成功,C++03编译失败
X<B> x3; // C++11编译成功,C++03编译失败
f(e1); // C++11编译成功,C++03编译失败
f(unnamed_obj); // C++11编译成功,C++03编译失败
f(b); // C++11编译成功,C++03编译失败
}在上面的代码中,由于结构体A和B都是局部类型,因此x1、x2和x3在C++11之前会编译失败。另外,因为e1、unnamed_obj的类型为匿名类型,所以f(e1)和f(unnamed_obj)在C++11之前也会编译失败。最后,由于b的类型是局部类型,因此f(b)在C++11之前同样无法编译成功。当然,在C++11上就没有以上的编译问题了。
在C++11标准之前,与局部和匿名类型不能作为模板实参同样没有道理的还有函数模板不能有默认模板参数的规则。说这条规则没有道理,是因为类模板是可以有默认模板参数的,而函数模板却不能,但却找不到一条要这么限制函数模板的理由。正因如此,这条限制在C++11标准中也被解除了。在C++11中,我们可以自由地在函数模板中使用默认的模板参数,甚至在语法上比类模板更加灵活:
template<class T = double>
void foo()
{
T t;
}
int main()
{
foo();
}在上面的代码中,函数模板foo有一个默认的模板参数double,所以在main函数中直接调用foo不会造成编译失败。因为在没有指定模板实参的时候它会使用默认的模板参数。值得注意的是,函数模板的默认模板参数是不会影响模板实参的推导的,也就是说推导出的类型的优先级高于默认参数,比如:
template<class T = double>
void foo(T t) {}
int main()
{
foo(5);
}在上面的代码中,虽然函数模板foo的默认模板参数是double,但是由于函数模板会根据函数实参推导模板实参类型,而且其优先级高于默认模板参数,因此这里相当于调用了foo(int)函数。
最后要说的是,函数模板的默认模板参数要比类模板的默认模板参数以及函数的默认参数都要灵活。我们知道无论是函数的默认参数还是类模板的默认模板参数,都必须保证从右往左定义默认值,否则无法通过编译,例如:
template<class T = double, class U, class R = double>
struct X {};
void foo(int a = 0, int b, double c = 1.0) {}以上代码由于模板参数U和参数b没有指定默认参数,破坏了必须从右往左定义默认值的规则,因此会编译失败。而函数模板就没有这个问题了:
template<class T = double, class U, class R = double>
void foo(U u) {}
int main()
{
foo(5);
}以上代码可以顺利地通过编译,其中T和R都有默认参数double,而U没有默认参数,不过U可以通过实参5推导出来。所以这里实际上调用的是foo<double, int, double>(int)函数。
在C++20标准之前,ADL的查找规则是无法查找到带显式指定模板实参的函数模板的,比如:
namespace N {
struct A {};
template <class T> int f(T) { return 1; }
}
int x = f<N::A>(N::A());MSVC会报错并提示找不到函数f,而GCC相对友好一些,它会报错并且询问是否要调用的是N::f。而CLang更加友好,它会编译成功,最后给出一条温馨的警告信息。
从C++20标准开始以上问题得以解决,编译器可以顺利地找到命名空间N中的函数f。不过需要注意的是,有些情况仍会让编译器报错,比如:
int h = 0;
void g() {}
namespace N {
struct A {};
template <class T> int f(T) { return 1; }
template <class T> int g(T) { return 2; }
template <class T> int h(T) { return 3; }
}
int x = f<N::A>(N::A()); // 编译成功,查找f没有找到任何定义,f被认为是模板
int y = g<N::A>(N::A()); // 编译成功,查找g找到一个函数,g被认为是模板
int z = h<N::A>(N::A()); // 编译失败在上面的代码中f和g都编译成功,因为根据标准要求编译器查找f和g的结果分别是什么都没找到以及找到一个函数,在这种情况下可以猜测它们都是模板函数,并且尝试匹配到命名空间N的f和g两个函数模板。而h则不同,编译器可以找到一个int变量h,在这种情况下紧跟h之后的<可以被认为是小于号,不符合标准要求,所以编译器仍会报错。
在C++20之前,非类型模板形参可以是整数类型、枚举类型、指针类型、引用类型和std::nullptr_t,但是类类型是无法作为非类型模板形参的,比如:
struct A {};
template <A a>
struct B {};
A a;
B<a> b; // 编译失败不过从C++20开始,字面量类类型(literal class)可以作为形参在非类型模板形参列表中使用了。具体要求如下。
1.所有基类和非静态数据成员都是公开且不可变的。
2.所有基类和非静态数据成员的类型是标量类型、左值引用或前者的(可能是多维)数组。
使用C++20的编译环境可以顺利编译上述代码,注意,到目前为止CLang还没有支持这项特性。
不知道读者是否曾经为非类型模板形参不能使用字符串字面量而感到遗憾呢?比如:
template <const char *>
struct X {};
X<"hello"> x; // 编译失败现在,我们可以利用字面量类类型以及其构造函数,让非类型模板形参间接地支持字符串字面量了,请看下面的代码:
template <typename T, std::size_t N>
struct basic_fixed_string
{
constexpr basic_fixed_string(const T(&foo)[N + 1])
{
std::copy_n(foo, N + 1, data_);
}
T data_[N + 1];
};
template <typename T, std::size_t N>
basic_fixed_string(const T(&str)[N])->basic_fixed_string<T, N - 1>;
template <basic_fixed_string Str>
struct X {
X() {
std::cout << Str.data_;
}
};
X<"hello world"> x;以上代码是在提案文档的示例上稍作修改,其中basic_fixed_string是一个典型的字面量类类型,它的构造函数接受一个常量字符串数组并将该数组复制到数据成员m_data中,因为构造函数声明为constexpr,所以可以在编译期执行完毕。接下来,代码通过自定义推导指引(详情请见第39章):
template <typename CharT, std::size_t N>
basic_fixed_string(const CharT(&str)[N])->basic_fixed_string<CharT, N - 1>;明确编译器通过构造函数推导模板实参的方法。然后将basic_fixed_string作为模板形参加入类模板X的模板形参列表中,这样编译器编译X<"hello world"> x;的时候就会根据basic_fixed_string的构造函数将"hello world"复制到data_中。最终,代码在运行期执行X的构造函数,输出字符串hello world。
一直以来,模板形参只能精确匹配实参列表,也就是说实参列表里的每一项必须和模板形参有着相同的类型。虽然这种匹配规则非常严谨且不易出错,但是却排除了很多合理的情况,比如:
template <template <typename> class T, class U> void foo()
{
T<U> n;
}
template <class, class = int> struct bar {};
int main()
{
foo<bar, double>();
}在上面的代码中,函数模板foo的模板形参列表接受一个模板实参,并且要求这个模板实参只有一个模板形参,巧的是类模板bar的模板形参列表中正好只有一个形参是需要指定的,而另外一个形参可以使用默认值。看起来foo<bar, double>()这种写法应该顺利地通过编译,但是事与愿违,这份代码在C++17之前是无法编译成功的。原因就是我们上文提到的:模板形参只能精确匹配实参列表,而这里类模板bar的模板形参数量与函数模板foo要求的模板实参的模板形参数量并不匹配,很明显这种匹配规则过于严苛了。
另外,由于在C++17中非类型模板形参可以使用auto作为占位符,因此我们可以写出这样的代码:
template <template <auto> class T, auto N> void foo()
{
T<N> n;
}
template <auto> struct bar {};
int main()
{
foo<bar, 5>();
}在上面的代码中,类型占位符auto最终都会被推导为int类型,于是模板形参和模板实参列表是匹配的,编译起来没有问题。但是修改一下函数模板foo,结果还是正确的吗?
template <template <int> class T, int N> void foo()
{
T<N> n;
}从推导的角度来看,类模板bar的模板形参中类型占位符auto被推导为int,这样一来整个推导过程似乎是顺理成章的,但是从匹配规则的角度来看又违反了必须精确匹配的要求,所以为了让以auto占位符作为非类型模板形参这个特性使用得更为广泛,也是时候对模板参数的匹配规则进行一些扩展了。
在C++17标准中放宽了对模板参数的匹配规则,它要求模板形参至少和实参列表一样特化。换句话说,模板形参可以和实参列表精确匹配。另外,模板形参也可以比实参列表更加特化。在新的匹配规则下,让我们重新审视上面的代码。
很显然,函数模板foo的模板形参template <typename> class T相较于实参template <class, class = int> struct bar更加特化。而模板形参template <int> class T相较于template <auto> struct bar也更加特化。这两份代码在C++17中都可以顺利地编译成功。
本章介绍的都是和模板参数相关的内容,其中允许常量求值作为非类型模板实参、允许局部和匿名类型作为模板实参和允许非类型模板形参中的字面量类类型扩展了模板参数的匹配范围,而函数模板添加到ADL查找规则和扩展的模板参数匹配规则则是优化了模板参数的匹配规则。掌握了这些特性能够让模板代码的编写更加得心应手,让模板完成之前不可能完成的任务,比如让字符串字面量作为模板实参就是一个典型的例子。