2.4.2、模板参数的缺省值
如果继续来搞模板的高度与宽度的参数,可能会想提供缺省的高度与宽度的非类型模板参数,就像前面在Grid<T>类模板的构造函数中一样。C++允许用类似的语法为模板参数提供缺省值。而我们正要这么做,那就也应该为T类型参数提供一个缺省值。下面是类定义:
export template <typename T = int, std::size_t WIDTH = 10, std::size_t HEIGHT = 10>
class Grid
{// Remainder is identical to the previous version
};
没有在成员函数的模板头中指定T,WIDTH,HEIGHT的缺省值。例如,下面是at()的实现:
template <typename T, std::size_t WIDTH, std::size_t HEIGHT>
const std::optional<T>& Grid<T, WIDTH, HEIGHT>::at(std::size_t x, std::size_t y) const
{verifyCoordinate(x, y);return m_cells[x][y];
}
有了这些改变,可以在没有任何模板参数,只要有元素类型,元素类型与宽度,或元素类型,宽度与高度等的情况下实例化Grid:
import grid;
import std;using namespace std;int main()
{Grid<> myIntGrid;Grid<int> myGrid;Grid<int, 5> anotherGrid;Grid<int, 5, 5> aFourthGrid;
}
注意如果不指定任何类模板参数,仍然需要指定一个空的<>。例如,下面的代码编译不成功!
Grid myIntGrid;
类模板参数列表的缺省参数规则与函数一样;也就是说,可以提供从右面开始的顺序参数的缺省值。
2.4.3、类模板参数推演
由于有了模板参数推演,编译器可以自动地从传递给灯模板构造函数的参数中推演出模板类型参数。
例如,标准库有一个类模板叫std::pair,在<utility>中定义。pair正好保存了两个可能的不同类型的值,一般会指定为模板类型参数。举例如下:
pair<int, double> pair1 { 1, 2.3 };
为了避免显式书写模板类型参数,辅助函数模板std::make_pair()可用。书写自己的函数模板的细节本章后面讨论。函数模板总是支持基于传递给函数模板的数值自动推演模板类型参数。这样的话,make_pair()就能够基于传递给它的值自动推演出模板类型参数。例如,对于如下的调用编译器推演出pair<int,double>:
auto pair2 { make_pair(1, 2.3) };
有了类模板参数推演(CTAD),这样的辅助函数模板就不再需要了。编译器现在基于传递给构造函数的参数自动推演出模板类型参数。对于pair类模板,可以只写下面的代码:
pair pair3 { 1, 2.3 }; // pair3 has type pair<int, double>
当然,只有在所有类模板的模板类型参数要么有缺省值,要么用于构造函数的参数时才有效,这样才可以推演。
注意要让CTAD工作,必须要进行初始化。下面的代码是非法的:
pair pair4;
许多标准库类支持CTAD,例如,vector,array,等等。
注意:类型推演对于std::unique_ptr与shared_ptr失效。传递T*给它们的构造函数,意味着编译器不得不在推演<T>或<T[]>之间进行选择,这是一个危险的要出错的选择。所以,只要记住对于unique_ptr与shared_ptr,需要继续使用make_unique()与make_shared()。
2.4.3.1、用户定义的推演指导
也可以书写自己的用户定义的推演指导来帮助编译器。允许书写规则如何让模板类型参数进行推演。下面是一个演示用法的例子。
假定有SpreadsheetCell类模板:
template <typename T>
class SpreadsheetCell
{
public:explicit SpreadsheetCell(T t) : m_content{ move(t) } { }const T& getContent() const { return m_content; }
private:T m_content;
};
由于有了CTAD,可以用std::string类型生成一个SpreadsheetCell。推演类型为SpreadsheetCell<string>:
string myString{ "Hello World!" };SpreadsheetCell cell{ myString };
然而,如果传递const char*给SpreadsheetCell构造函数,类型T推演为const char*,这不是你想要的!可以生成如下用户定义的推演指导确保当传递const char*作为参数时给到构造函数时能够推演出std::string:
SpreadsheetCell(const char*) -> SpreadsheetCell<std::string>;
该指导要定义在类定义之外,但要在与SpreadsheetCell类相同的命名空间内。
通用语法如下。explicit关键字是可选的,与构造函数中的explicit的行为一样。这样的推演指导,总比没有强,也是模板的内容。
template <...>
explicit TemplateName(Parameters) -> DeducedTemplate<...>;
2.5、成员函数模板
C++允许参数化类的单个成员函数。这样的成员函数叫做成员函数模板,可以在一个正常娄或类模板内。当写成员函数模板时,实际上是在写不同类型的成员函数的许多不同版本。成员函数模板在类模板中对于赋值操作符与拷贝构造函数是有用的。
警告:虚成员函数与析构函数不能是成员函数模板。
考虑原来的Grid模板,只有一个模板参数:元素类型。可以实例化grid为许多不同的类型,比如int与double:
Grid<int> myIntGrid;
Grid<double> myDoubleGrid;
然而,Grid<int>与Grid<double>是两种不同的类型。如果写一个函数使用Grid<double>类型的对象,不能传递Grid<int>。即使知道int grid元素也是可以拷贝到double grid元素中的,因为int可以被转换为double,不能将Grid<int>类型的对象赋值给Grid<double>类型的对象,或者从Grid<int>中构造Grid<double>。下面的两行代码编译不会成功:
myDoubleGrid = myIntGrid; // DOES NOT COMPILE
Grid<double> newDoubleGrid { myIntGrid }; // DOES NOT COMPILE
问题是Grid模板的拷贝构造函数与赋值操作符如下:
Grid(const Grid& src);
Grid& operator=(const Grid& rhs);
等价于:
Grid(const Grid<T>& src);
Grid<T>& operator=(const Grid<T>& rhs);
Grid拷贝构造函数与operator=两者都使用const Grid<T>的引用。当实例化Grid<double>并且尝试调用拷贝构造函数与operator=时,编译器用它们的原型来生成成员函数:
Grid(const Grid<double>& src);
Grid<double>& operator=(const Grid<double>& rhs);
在生成的Grid<double>类中没有构造函数或operator=使用Grid<int>。
万幸的事,可以通过添加拷贝构造函数的参数化版本与grid<int>类模板的赋值操作符来生成成员函数从一种grid类型转化为另一种来纠正这种疏忽。下面是新的Grid类模板定义:
export template <typename T>
class Grid
{
public:template <typename E>Grid(const Grid<E>& src);template <typename E>Grid& operator=(const Grid<E>& rhs);void swap(Grid& other) noexcept;// Omitted for brevity
};
原来的拷贝构造函数与拷贝赋值操作符不能移除。编译器在E等于T时不会调用这些新的参数化的拷贝构造函数与参数化的拷贝赋值操作符。
首先检查新的参数化的拷贝构造函数:
template <typename E>
Grid(const Grid<E>& src);
可以看到有另外一个带有不同类型名字的模板头,E(元素的简写)。类在一种类型T在参数化,新的拷贝构造函数在另一种类型E上额外参数化。两层参数化允许拷贝一种类型的grid到另一种类型。下面是新的拷贝构造函数的定义:
template <typename T>
template <typename E>
Grid<T>::Grid(const Grid<E>& src): Grid{ src.getWidth(), src.getHeight() }
{// The ctor-initializer of this constructor delegates first to the// non-copy constructor to allocate the proper amount of memory.// The next step is to copy the data.for (std::size_t i{ 0 }; i < m_width; ++i) {for (std::size_t j{ 0 }; j < m_height; ++j) {at(i, j) = src.at(i, j);}}
}
可以看到,在成员模板头(带有E参数)之前必须声明类模板头(带有T参数)。不能把它们组合成下面这样:
template <typename T, typename E> // Wrong for nested template constructor!
Grid<T>::Grid(const Grid<E>& src)
在构造函数定义之前额外的模板头,注意必须使用公共的访问成员函数getWidth(),getHeight(),与at()来访问src的元素。这是因为拷贝目的的对象是Grid<T>类型,而拷贝源的对象类型为Grid<E>。它们不是同一种类型,所以必须使用公共成员函数。
swap()成员函数就比较直接了:
template <typename T>
void Grid<T>::swap(Grid& other) noexcept
{std::swap(m_width, other.m_width);std::swap(m_height, other.m_height);std::swap(m_cells, other.m_cells);
}
参数化的赋值操作符使用的是const Grid<E>&,但返回的是Grid<T>&:
template <typename T>
template <typename E>
Grid<T>& Grid<T>::operator=(const Grid<E>& rhs)
{// Copy-and-swap idiomGrid<T> temp{ rhs }; // Do all the work in a temporary instance.swap(temp); // Commit the work with only non-throwing operations.return *this;
}
该赋值操作符的实现使用了copy-and-swap习语。swap()成员函数只能交换相同类型的Grid,但是因为参数化的赋值操作符首先将给定的Grid<E>转化为了Grid<T>叫做temp,使用了参数化的拷贝构造函数,也是没有问题的。这之后,它使用了swap()成员函数来用this交换这个临时的grid<T>,this也是Grid<T>类型。