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

在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 {};,则可能会发生编译错误。具体情况要根据编译器厂商和版本而定,比如在GCC新版本中这种写法都是允许的,而CLang的新版本也只会给出一个警告,只有在它们的老版本中才会给出错误提示。总之,在C++17之前typename的这种写法是不符合标准的。

其实,这种严苛的规则在过去看来是顺理成章的。因为在过去,能作为模板形参的只有类模板,并没有其他可能性,所以规定必须使用class来声明模板形参是合情合理的。但是自从C++11标准诞生,随着别名模板的引入,类模板不再是模板形参的唯一选择了,例如:

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

可以看到,这里的A实际上就是int类型而不是一个类模板。很明显,现在已经没有必要强调必须使用class来声明模板形参了,删除这个规则可以让语言更加简单合理。所以在C++17标准中使用typename来声明模板形参已经不是问题了:

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

我们知道当使用未决类型的内嵌类型时,例如X<T>::Y,需要使用typename明确告知编译器X<T>::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::BD() : T::B() {}都没有指定typename,但是编译器依然可以正确地识别程序意图。实际上,除了以上两种情况外,还有很多时候也可以从语义中明确地判断出X<T>::Y表示的是类型,比如使用using创建类型别名的时候,using R = typename T::B;typename完全没有存在的必要。

在C++20标准中,增加了一些情况可以让我们省略typename关键字。

1.在上下文仅可能是类型标识的情况,可以忽略typename

  static_castconst_castreinterpret_castdynamic_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(); } // 编译成功
};

最后需要提出的是,到目前为止实现了这部分特性的编译器只有GCC而已,至于CLang和MSVC编译以上代码依然会报错,并且提示需要添加typename

本章的内容虽然比较简单,但是对于爱好模板元编程的读者来说却有一定意义。要知道模板元编程可以说是对类型的编程,所以在模板元编程的代码中总是会出现成堆的typename关键字,这些冗余的描述增加了无谓的代码量,非常影响代码的整洁。C++20标准减少typename声明的必要性无疑减轻了这种负担。允许使用typename声明模板形参也让模板声明体系显得更加合理了。