• 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没有定义可变参数版本。

  • 相关阅读:
    13-02 Java 数组高级算法,Arrays类
    从0移植uboot (二) _uboot启动流程分析
    Linux input子系统编程、分析与模板
    跟着内核学框架-从misc子系统到3+2+1设备识别驱动框架
    Linux驱动技术(八) _并发控制技术
    Linux驱动技术(七) _内核定时器与延迟工作
    Linux驱动技术(六) _内核中断
    Linux驱动技术(五) _设备阻塞/非阻塞读写
    Linux驱动技术(四) _异步通知技术
    Linux驱动技术(三) _DMA编程
  • 原文地址:https://www.cnblogs.com/fortunely/p/15919062.html
Copyright © 2020-2023  润新知