C++ Primer学习笔记 - 第16章 模板与泛型编程(二)

上半部分,见C++ Primer学习笔记 - 第16章 模板与泛型编程(一)

16.3 重载与模板

函数模板可以被另一个模板或普通非模板函数重载。跟普通函数重载一样,名字相同的函数必须具有不同数量或类型的参数。

如果涉及到函数模板,则函数匹配规则会在下面几个方面受到影响:

  • 对于一个调用,其候选函数包括所有模板实参推断成功的函数模板实例。
  • 候选的函数模板总是可行的,因为模板实参推断会排除任何不可行的模板。
  • 与往常一样,可行函数(模板与非模板)按类型转换(如果对此调用需要的话)来排序。当然,可用于函数模板调用的类型转换非常有限,只有const转换、数组或函数指针转换,见16.2.1
  • 与往常一样,如果恰有一个函数提供比任何其他函数都更好的匹配,则选择此函数。但是,如果有多个函数提供同样好的匹配,则:
    1)如果同样好的函数中只有一个是非模板函数,则选择此函数。
    2)如果同样好的函数中没有非模板函数,而有多个函数模板,且其中一个模板比其他模板更特例化,则选择此模板。
    3)否则,则此调用有歧义。

编写重载模板

我们定义2个版本的函数模板debug_rep,用来打印我们不能处理的类型。第一个模板接受一个const对象的引用,第二个模板接受一个指针类型。

// 第一个版本函数模板
template <typename T> string debug_rep(const T& t)
{
    ostringstream ret;
    ret << t;           // 使用T的输出运算符打印t的一个表示形式
    return ret.str();   // 返回ret绑定的string的一个副本
}

// 第二个版本函数模板
// 注意:此函数不能用于char*
template <typename T> string debug_rep(T* p)
{
    ostringstream ret;
    ret << "pointer: " << p;         // 打印指针本身的值(地址)
    if (p)
        ret << " " << debug_rep(*p); // 打印p指向的值
    else
        ret << " null pointer";      // 或指出p为空
    return ret.str();                // 返回ret绑定的string的一个副本
}

注意:第二个版本虽然接受指针类型,但不能用于打印C风格字符串指针,因为IO库为char*值定义了一个<<版本,该版本假定指针表示一个空字符结尾的字符数组,并打印数组内容而非地址值。因此虽然打印C风格字符串指针可能不会出错,但打印的内容并不符合预期。

如何使用上面定义的函数?

string s("hi");
cout << debug_rep(s) << endl; // s非指针,因此从第一个模板实例化函数

cout << debug_rep(&s) << endl; // s为指针,因此从第二个模板实例化函数

debug_rep(&s)也能用第一个版本生成实例,为什么编译器会选择第二个版本?
因为,虽然两个函数都能生成可行的实例:

  • debug_rep(const string&),由第一个版本的debug_rep实例化而来,T被绑定到string
  • debug_rep(string*),由第二个版本的debug_rep实例化而来,T被绑定到string。

但是,第二个版本的debug_rep实例是此调用的精确匹配。第一个版本的实例需要进行普通指针(string)到const指针(const string&)的转换。正常函数匹配规则告诉我们应当选择第二个模板。

多个可行模板

另外一个调用:

const string* sp = &s;
cout << debug_rep(sp) << endl;

此例中,2个版本的实例都是精确匹配:

  • debug_rep(const string&),由第一个版本的debug_rep实例化而来,T被绑定到string
  • debug_rep(const string*),由第二个版本的debug_rep实例化而来,T被绑定到const string。

此时,正常函数匹配规则无法区分这2个函数,我们可能会决定这个调用有二义性。但,根据重载函数模板的特殊规则,此调用被解析为debug_rep(T*),即,更特例化的版本。

设计这条规则的原因:没有它,将无法对一个const指针调用指针版本的debug_rep。
模板debug_rep(const T&)本质上可以用于任何类型,包括指针类型。该模板比debug_rep(T*)更通用,后者只能用于指针类型,也就是说后者更加特例化。

PS:当有多个重载模板对一个调用提供同样好的匹配时,应选择最特例化的版本。

非模板和模板重载

定义一个普通的debug_rep(非模板),来打印双引号包围的string:

// 定义普通函数版本的debug_rep 
// 打印双引号包围的string
string debug_rep(const string& s)
{
    return '"' + s + '"';
}

此时,同样有2个可行函数:

  • debug_rep(const string&),第一个模板,T被绑定到string。
  • debug_rep(const string&),普通非模板函数。

此时,虽然2个函数具有相同参数列表,具有同样好匹配,但编译器会选择非模板版本。因为有多个同样匹配的函数时,编译器会选择最特例化的版本,也就是普通非模板函数。

PS:对于一个调用,如果一个非函数模板与一个函数模板提供同样的匹配,则选择非模板版本。因为前者更特例化。

重载模板和类型转换

有一种情况还没讨论到:C风格字符串指针和字符串字面常量。

考虑调用:

cout << debug_rep("hi world!") << endl; // 调用debug_rep(T*)

本例中,所有三个debug_rep版本都是可行的:

  • debug_rep(const T&),T被绑定到char[10]。
  • debug_rep(T*),T被绑定到const char。
  • debug_rep(const string&),要求从const char*到string的类型转换。

编译器会选择第二个版本实例,因为这个是最特例化的。

然而,第二个版本并不会将字符串按string处理,因为IO库为char*提供了专门的<<,会假设字符串以NUL-byte结尾。
如果希望将字符串按string处理,可以定义另外两个非模板重载版本:

// 将C字符串指针转换为string,并调用string版本的debug_rep(转交给另外一个版本的debug_rep处理)
string debug_rep(char* p)
{
  return debug_rep(string(p));
}
string debug_rep(const char* p)
{
  return debug_rep(string(p));
}

缺少声明可能导致程序行为异常

对于重载函数模板的函数,如果忘记了声明的函数,编译器可以从模板实例化出与调用匹配的版本,从而导致难以察觉的错误。因此,必须保证调用的函数模板,在作用域内有对应声明的函数。
比如,为了使char*版本的debug_rep 正确工作,定义此版本时,debug_rep(const string&)的声明必须在作用域中;否则,可能调用错误的debug_rep版本

template <typename T> string debug_rep(const T& t);
template <typename T> string debug_rep(T* p);
// 为使debug_rep(char*)定义正确的构造,下面的声明必须在作用域中
string debug_rep(const string& s);
string debug_rep(char* p)
{
    return debug_rep(string(p));
}

16.4 可变参数模板

一个可变参数模板(variadic template)是一个接受可变数目参数的模板函数或模板类。可变数目的参数被称为参数包(parameter packet)。存在两种参数包:模板参数包(template parameter packet),表示0个或多个模板参数;函数参数包(function parameter packet),表示0个或多个函数参数。

怎么表示一个模板参数或函数参数的包?
可以用一个省略号来指出一个模板参数或函数参数表示一个包,如class...或typename...,指出接下来的参数表示0个或多个类型的列表;一个类型名后面跟一个省略号表示0个或多个给定类型的非类型参数的列表。

// Args 是一个模板参数包;rest是一个函数参数包
// Args 表示零个或多个模板类型参数
// rest 表示零个或多个函数参数
template <typename T, typename... Args>    // 这里... 指出Args是一个模板参数包
void foo(const T& t, const Args&... rest); // 这里... 指出rest是一个函数参数包,Args&是其类型

上面的语句声明了foo是一个可变参数函数模板,有一个名为T的类型参数,和一个名为Args的模板参数包。这个包表示零个或多个额外的类型参数。foo的函数参数列表包含一个const&类型的参数,指向T的类型,还包含一个名为rest的函数参数包,此包表示零个或多个函数参数。

编译器会从函数的实参推断模板参数类型。对于一个可变参数模板,编译器还会推断包中参数的数目。例如,下面的调用:

int i = 0; double d = 3.14; string s = "how now brown cow";
foo(i, s, 42, d);
foo(s, 42, d);
foo(d, s);
foo("hi");

编译器会为foo实例化出4个不同的版本:

void foo(const int&, const string&, const int&, const double&);
void foo(const string&, const int&, const char[3]&);
void foo(const double&, const string&);
void foo(const char[3]&);

每个实例中,T的类型都是从第一个实参的类型推断出来的。剩下的实参(如果有的话)提供函数额外实参的数目和类型。

sizeof...运算符

当我们想知道包中有多少元素时,该怎么办?
可以使用sizeof...运算符。类似于sizeof,sizeof...运算符也返回一个常量表达式,而且不会对实参求值:

比如,可以直接用上面的例子,

// Args 是一个模板参数包;rest是一个函数参数包
// Args 表示零个或多个模板类型参数
// rest 表示零个或多个函数参数
template <typename T, typename... Args>
void foo(const T& t, const Args&... rest)
{
    cout << sizeof...(Args) << endl; // 类型参数的数目
    cout << sizeof...(rest) << endl; // 函数参数的数目
}

...
foo(i, s, 42, d); // 打印3,3
foo(s, 42, d);    // 打印2,2
foo(d, s);        // 打印1,1
foo("hi");        // 打印0,0

PS:sizeof...求参数包中参数个数时,只能在模板内或函数内使用。

16.4.1 编写可变参数函数模板

我们知道initializer_list可定义一个可接受可变数目实参的函数,但是initializer_list有其局限:所有实参必须具有相同类型(或它们的类型可以转换为一个公共类型)。
当我们既不知道要处理的实参数目,也不知道其类型时,可变参函数就很有用。而可变参函数模板在这方面,就比较有效。

我们定义一个print函数,它在一个给定流上打印给定实参列表的内容。
可变参数函数通常是递归的。第一步,调用处理包中的第一个实参,然后用剩余实参调用自身。print函数也是这样的模式,每次递归调用将第二个实参打印到第一个实参表示的流中。为终止递归,我们还需要定义一个非可变参数的print函数,它接受一个流和一个对象:

// 用来终止递归并打印最后一个元素的函数
// 此函数必须在可变参数版本的print定义之前声明
template <typename T>
ostream& print(ostream& os, const T& t)
{
    return os << t;
}

// 包中除了最后一个元素外的其他元素都会调用这个版本的print
template <typename T, typename... Args>
ostream& print(ostream& os, const T& t, const Args&... rest)
{
    os << t << ", ";           // 打印第一个实參
    return print(os, rest...); // 递归调用,打印其他实參
}

// 调用示例
print(cout, 2, "hello", "a"); // 打印 2, hello, a

PS:当定义可变参数版本的print时,非可变参数版本的声明必须在作用域中。否则,可变参数版本会无限递归。

16.4.2 包扩展

对于一个参数包,除了用sizeof...获取其大小(实参个数),能对它做的唯一事情就是扩展(expand)它。
当扩展一个包时,还有提供用于每个扩展元素的模式(pattern)。扩展一个包,就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式右边放一个省略号(...)来触发扩展操作。

例如,自定义print函数包含2个扩展:

template <typename T, typename... Args>
ostream& print(ostream& os, const T& t, const Args&... rest)   // 扩展Args
{
  os << t << ",";
  return print(os, rest...);                                   // 扩展rest
}

第一个扩展操作扩展模板参数包(Args),为print生成函数参数列表。
第二个扩展操作出现在对print的调用中。此模式为print调用生成实参列表。

在对Args的扩展中,编译器将模式const Args&应用到模板参数包Args中的每个元素。因此,此模式的扩展结果是一个逗号分隔的零个或多个类型的列表,每个类型都形如const type&。例如:

print(cout, i, s, 42); // 包中有2个参数

最后2个实参的类型和模式一起确定了尾置参数的类型。此调用被实例化为:

ostream& print(ostream&, const int&, const string&, const int&);

理解包扩展

前面的print中函数参数包通过递归方式,仅仅将包扩展为其构成元素,C++还允许更复杂的扩展模式。

例如,我们可以编写第二个可变参数函数,对其每个实参调用debug_rep,然后调用print打印结果string:

// 在print调用中对每个实參调用debug_rep
template <typename... Args>
ostream& errorMsg(ostream& os, const Args&... rest)
{
//    print(os, debug_rep(a1), debug_rep(a2), ..., debug_rep(an));
    return print(os, debug_rep(rest)...);
}

该print调用使用了模式debug_rep(rest)。此模式表示我们希望对函数参数包rest中的每个元素调用debug_rep。扩展结果是将一个逗号分隔的debug_rep调用列表。
例如,调用:

errorMsg(cerr, fcnName, code.num(), otherData, "other", item);

就好像我们这样编写代码:

print(cerr, debug_rep(fcnName), debug_rep(code.num()),
      debug_rep(otherData), debug_rep("other"),
      debug_rep(item));

相对的,下面的模式会编译失败:

// 将包传递给debug_rep; print(os, debug_rep(a1, a2, ..., an))
print(os, debug_rep(rest...)); // 错误:此调用无此匹配函数

这段代码问题在哪儿?
问题在于我们在debug_rep调用中扩展了rest,也就是说,这段代码等价于:

print(os, debug_rep(fcnName, code.num(), otherData, "otherData", item)); // 错误:debug_rep并没有定义可变参数版本

显然,这段等价代码是错误的,因为debug_rep没有定义可变参数版本。

posted @ 2022-02-21 22:42  明明1109  阅读(96)  评论(0编辑  收藏  举报