现代C++语言核心特性解析part20

第36章 typename优化(C++17 C++20)

36.1 允许使用typename声明模板形参

在C++17标准之前,必须使用class来声明模板形参,而typename是不允许使用的。

template <typename T> struct A {};
template <template <typename> class T> struct B {};
int main()
{
	B<A> ba;
}

上面的代码可以顺利地编译通过,但是如果将B的定义修改为template <template < typename> typename T> struct B{};,则可能会发生编译错误。
自从C++11标准诞生,随着别名模板的引入,类模板不再是模板形参的唯一选择了,例如:

template <typename T> using A = int;
template <template <typename> class T> struct B {};
int main()
{
	B<A> ba;
}

在C++17标准中使用typename来声明模板形参已经不是问题了:

template <typename T> using A = int;
template <template <typename> typename T> struct B {};
int main()
{
	B<A> ba;
}

36.2 减少typename使用的必要性

当使用未决类型的内嵌类型时,例如X::Y,需要使用typename明确告知编译器X::Y是一个类型,否则编译器会将其当作一个表达式的名称

template<class T> void f(T::R);
template<class T> void f(typename T::R);

在C++20标准之前,只有两种情况例外,它们分别是指定基类和成员初始化,例如::

struct Impl {};
struct Wrap {
	using B = Impl;
};
template<class T>
struct D : T::B {
	D() : T::B() {}
};
D<Wrap> var;

在上面的代码中struct D : T::B和D() : T::B() {}都没有指定typename,但是编译器依然可以正确地识别程序意图。
在C++20标准中,增加了一些情况可以让我们省略typename关键字。
1.在上下文仅可能是类型标识的情况,可以忽略typename。static_cast、const_cast、reinterpret_cast或dynamic_cast等类型转换:

static_cast<T::B>(p);

定义类型别名:

using R = T::B;

后置返回类型:

auto g() -> T::B;

模板类型形参的默认参数:

template <class R = T::B> struct X;

2.还有一些声明的情况也可以忽略typename。
全局或者命名空间中简单的声明或者函数的定义:

template<class T> T::R f();

结构体的成员:

template<class T>
struct D : T::B {
D() : T::B() {}
	T::B b; // 编译成功
};

作为成员函数或者lambda表达式形参声明:

template<class T>
struct D : T::B {
	D() : T::B() {}
	T::B f(T::B) { return T::B(); } // 编译成功
};

第37章 模板参数优化(C++11 C++17 C++20)

37.1 允许常量求值作为所有非类型模板的实参

相对于以类型为模板参数的模板而言,以非类型为模板参数的模板实例化规则更加严格。在C++17标准之前,这些规则包括以下几种。
1.如果整型作为模板实参,则必须是模板形参类型的经转换常量表达式。所谓经转换常量表达式是指隐式转换到某类型的常量表达式,特点是隐式转换和常量表达式。

constexpr char v = 42;
constexpr char foo() { return 42; }
template<int> struct X {};
int main()
{
	X<v> x1;
	X<foo()> x2;
}

2.如果对象指针作为模板实参,则必须是静态或者是有内部或者外部链接的完整对象。
3.如果函数指针作为模板实参,则必须是有链接的函数指针。
4.如果左值引用的形参作为模板实参,则必须也是有内部或者外部链接的。
5.而对于成员指针作为模板实参的情况,必须是静态成员。

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()的返回值类型,根据第一条规则它只会接受整型的经转换常量表达式。
新的标准只强调了一条规则:非类型模板形参使用的实参可以是该模板形参类型的任何经转换常量表达式。其中经转换常量表达式的定义添加了对象、数组、函数等到指针的转换。这从另一个角度对以前的规则进行了兼容。
由于规则的修改,还带来了一个有趣的变化。仔细观察新规则会发现,现在对于指针不再要求是具有链接的,取而代之的是必须满足经转换常量表达式求值。

template<const char *> struct Y {};
int main()
{
	static const char str[] = "hello world";
	Y<str> y;
}

新规则并非万能,以下对象作为非类型模板实参依旧会造成编译器报错。
1.对象的非静态成员对象。
2.临时对象。
3.字符串字面量。
4.typeid的结果。
5.预定义变量。

37.2 允许局部和匿名类型作为模板实参

在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之前同样无法编译成功。

37.3 允许函数模板的默认模板参数

在C++11中,我们可以自由地在函数模板中使用默认的模板参数

template<class T = double>
void foo()
{
	T t;
}
int main()
{
	foo();
}

函数模板的默认模板参数是不会影响模板实参的推导的,也就是说推导出的类型的优先级高于默认参数

template<class T = double>
void foo(T t) {}
int main()
{
	foo(5);
}

无论是函数的默认参数还是类模板的默认模板参数,都必须保证从右往左定义默认值,否则无法通过编译

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);
}

37.4 函数模板添加到ADL查找规则

在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之后的<可以被认为是小于号,不符合标准要求,所以编译器仍会报错。

37.5 允许非类型模板形参中的字面量类类型

在C++20之前,非类型模板形参可以是整数类型、枚举类型、指针类型、引用类型和std::nullptr_t,但是类类型是无法作为非类型模板形参的

struct A {};
template <A a>
struct B {};
A a;
B<a> b; // 编译失败

从C++20开始,字面量类类型(literal class)可以作为形参在非类型模板形参列表中使用了。具体要求如下。
1.所有基类和非静态数据成员都是公开且不可变的。
2.所有基类和非静态数据成员的类型是标量类型、左值引用或前者的(可能是多维)数组。

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,所以可以在编译期执行完毕。然后将basic_fixed_string作为模板形参加入类模板X的模板形参列表中,这样编译器编译X<“hello world”> x;的时候就会根据basic_fixed_string的构造函数将"hello world"复制到data_中。最终,代码在运行期执行X的构造函数,输出字符串hello world。

37.6 扩展的模板参数匹配规则

一直以来,模板形参只能精确匹配实参列表,也就是说实参列表里的每一项必须和模板形参有着相同的类型。虽然这种匹配规则非常严谨且不易出错,但是却排除了很多合理的情况,比如:

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之前是无法编译成功的。
由于在C++17中非类型模板形参可以使用auto作为占位符,因此我们可以写出这样的代码:

template <template <auto> class T, auto N> void foo()
{
	T<N> n;
}
template <auto> struct bar {};
int main()
{
	foo<bar, 5>();
}

在C++17标准中放宽了对模板参数的匹配规则,它要求模板形参至少和实参列表一样特化。换句话说,模板形参可以和实参列表精确匹配。另外,模板形参也可以比实参列表更加特化。
函数模板foo的模板形参template < typename> class T相较于实参template <class, class = int> struct bar更加特化。而模板形参template < int> class T相较于template < auto> struct bar也更加特化。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值