美文网首页
01 函数模板

01 函数模板

作者: 奇点创客 | 来源:发表于2021-05-20 13:40 被阅读0次

    函数模板示例

    • 最简单的例子如下。使用作用域运算符(::)表示指定使用全局命名空间中的 max 模板,而非 std::max
    #include <iostream>
    #include <string>
    
    template <typename T>
    T max(T a, T b) {
      return b < a ? a : b;
    }
    
    int main() {
      std::cout << ::max(1, 3);       // 3
      std::cout << ::max(1.0, 3.14);  // 3.14
      std::string s1 = "mathematics";
      std::string s2 = "math";
      std::cout << ::max(s1, s2);  // mathematics
    }
    

    Two-Phase Translation

    • 模板被编译两次,分别发生在实例化前和实例化期间
    • 实例化前检查模板代码本身,包括
      • 检查语法是否正确,如是否遗漏分号
      • 发现使用不依赖于模板参数的 unknown name(类型名、函数名......)
      • 检查不依赖于模板参数的 static assertion
    • 实例化期间,再次检查模板代码保证所有代码有效(如某个类型不支持 operator<),特别的是,会再次检查依赖于模板参数的部分
    template <typename T>
    void f(T x) {
      undeclared();   // first-phase compile-time error if undeclared() unknown
      undeclared(x);  // second-phase compile-time error if undeclared(T) unknown
      static_assert(sizeof(int) > 10,
                    "int too small");  // always fails if sizeof(int) <= 10
      static_assert(sizeof(T) > 10,
                    "T too small");  // fails if instantiated for T with size <= 10
    }
    

    模板实参推断(Template Argument Deduction)

    • 如果传递 int 给 const T&,T 会被推断为 int
    template <typename T>
    T max(const T& a, const T& b) {
      return b < a ? a : b;
    }
    
    std::cout << ::max(1, 42);  // int max<int>(const int&, const int&)
    
    • 实参的推断不允许自动类型转换,要求每个 T 都要正确匹配。传值调用参数时,cv 限定符会被忽略,引用转换为对应的值类型,数组或函数转换为指针
    template <typename T>
    T max(T a, T b) {
      return b < a ? a : b;
    }
    
    int a = 1;
    const int b = 42;
    int& c = a;
    int arr[4];
    std::string s;
    
    ::max(a, b);        // OK:T 推断为 int
    ::max(b, b);        // OK:T 推断为 int
    ::max(a, c);        // OK:T 推断为 int
    ::max(&a, arr);     // OK:T 推断为 int*
    ::max(1, 3.14);     // 错误:T 推断为 int 和 double
    ::max("hello", s);  // 错误:T 推断为 const char[6] 和 std::string
    
    • 两个参数类型不同时,有三个解决方案
      • 强制转换参数类型:max(static_cast<double>(1), 3.14)
      • 指定 T:max<double>(1, 3.14)
      • 用两个模板参数指定不同类型
    • 类型推断对默认实参无效,在圆括号的参数列表中指定T的默认实参是无效的
    template <typename T>
    void f(T = "");
    
    f(1);  // OK:T 推断为 int,调用 f<int>(1)
    f();   // 错误:不能推断 T
    
    • 默认实参应该在尖括号的模板参数列表中声明
    template <typename T = std::string>
    void f(T = "");
    
    f();  // OK
    

    多种模板参数(Multiple Template Parameters)

    • 函数模板有两种参数,尖括号里的 T 叫模板参数(template parameter),参数列表里的 T 叫调用参数(call parameter),用来替换模板参数的各个对象叫模板实参,如 double
    • 模板参数数量不限,但不能指定默认的模板实参(对于函数模板而非类模板),如对于上述问题,可以指定两个类型,但返回类型为 T 不一定符合调用者的意图
    template <typename T, typename U>
    T max(T a, U b) {
      return b < a ? a : b;
    }
    
    auto m = ::max(1, 3.14);  // 返回类型由第一个实参决定
    
    • 模板实参不能推断返回类型,必须显式指定
    template <typename T, typename U, typename RT>
    RT max(T a, U b) {  // RT 不能被推断出
      return b < a ? a : b;
    }
    
    ::max(1, 3.14);                       // 错误
    ::max<int, double, double>(1, 3.14);  // OK:显式指定 RT 为 double
    
    • 但前两个模板参数被推断,没必要显式指定,因此可以改变模板参数声明顺序,把 RT 放到最前,这样使用时只需要显式指定一个模板实参
    template <typename RT, typename T, typename U>
    RT max(T a, U b) {
      return b < a ? a : b;
    }
    
    ::max<double>(1, 3.14);  // OK:返回类型为 double,返回 3.14
    
    • C++14 允许 auto 作为返回类型,它通过 return 语句推断返回类型
    template <typename T, typename U>
    auto max(T a, U b) {
      return b < a ? a : b;
    }
    
    • 如果只支持 C++11,还需要指定尾置返回类型
    template <typename T, typename U>
    auto max(T a, U b) -> decltype(b < a ? a : b) {
      return b < a ? a : b;
    }
    
    • 用 true 作为条件也一样,需要的只是推断类型而已
    template <typename T, typename U>
    auto max(T a, U b) -> decltype(true ? a : b) {
      return b < a ? a : b;
    }
    
    #include <type_traits>
    
    template <typename T, typename U>
    typename std::common_type<T, U>::type max(T a, U b) {
      return b < a ? a : b;
    }
    
    • C++14 中能简写为
    #include <type_traits>
    
    template <typename T, typename U>
    std::common_type_t<T, U> max(T a, U b) {
      return b < a ? a : b;
    }
    
    • 对于较为复杂的类型,可以写到一个模板参数中
    #include <type_traits>
    
    template <typename T, typename U, typename RT = std::common_type_t<T, U>>
    RT max(T a, U b) {
      return b < a ? a : b;
    }
    
    • 有时 T 必须是一个引用,但返回类型是引用类型时可能出现问题,此时可以利用 std::decay去掉引用修饰符、cv 限定符等描述符,退化到最基本的类型
    #include <type_traits>
    
    template<typename T, typename U, typename RT = std::decay_t<decltype(true ? T() : U())>>
    RT max(T a, U b) {
      return b < a ? a : b;
    }
    
    • 调用时即可不指定模板参数
    auto a = ::max(1, 3.14);
    

    函数模板的重载

    int max(int a, int b) { return b < a ? a : b; }
    
    template <typename T>
    T max(T a, T b) {
      return b < a ? a : b;
    }
    
    ::max(1, 42);          // 调用函数
    ::max('a', 3.14);      // 调用函数
    ::max(1.0, 3.14);      // 通过推断调用 max<double>
    ::max('a', 'b');       // 通过推断调用 max<char>
    ::max<>(1, 42);        // 通过推断调用 max<int>
    ::max<double>(1, 42);  // 调用 max<double>
    
    • 返回类型的重载可能导致调用的歧义
    template <typename T, typename U>
    auto max(T a, U b) {
      return b < a ? a : b;
    }
    
    template <typename RT, typename T, typename U>
    RT max(T a, U b) {
      return b < a ? a : b;
    }
    
    auto a = ::max(1, 3.14);               // 调用第一个模板
    auto b = ::max<long double>(3.14, 1);  // 调用第二个模板
    auto c = ::max<int>(1, 3.14);          // 错误:两个模板都匹配
    // 模板一为double max<int, double>, 模板二为int max<int, double>
    
    • 指针或传统 C 字符串的重载
    #include <cstring>
    #include <string>
    
    template <typename T>
    T max(T a, T b) {  // 1
      return b < a ? a : b;
    }
    
    template <typename T>
    T* max(T* a, T* b) {  // 2
      return *b < *a ? a : b;
    }
    
    const char* max(const char* a, const char* b) {  // 3
      return std::strcmp(b, a) < 0 ? a : b;
    }
    
    int main() {
      int a = 1;
      int b = 42;
      ::max(a, b);    // 调用 1,max<int>
      ::max(&a, &b);  // 调用 2,max<int>
    
      std::string s1 = "hello";
      std::string s2 = "world";
      ::max(s1, s2);  // 调用 1,max<std::string>
    
      const char* x = "hello";
      const char* y = "world";
      ::max(x, y);  // 调用 3
    }
    
    • 如果用传引用实现模板,再用传值重载 C 字符串版本,不能用三个实参版本计算三个 C 字符串的最大值
    #include <cstring>
    
    template <typename T>
    const T& max(const T& a, const T& b) {
      return b < a ? a : b;
    }
    
    const char* max(const char* a, const char* b) {
      return std::strcmp(a, b) < 0 ? b : a;
    }
    
    template <typename T>
    const T& max(const T& a, const T& b, const T& c) {
      return max(max(a, b), c);  // 如果传值调用 max(a, b) 则出错
    }
    
    int main() {
      const char* a = "frederic";
      const char* b = "anica";
      const char* c = "lucas";
      ::max(a, b, c);  // run-time ERROR
    }
    
    • 错误原因是 max(max(a, b), c) 中,max(a, b) 产生了一个临时对象的引用,这个引用在计算完就马上失效了
    • 重载版本必须在函数调用前声明才可见
    #include <iostream>
    
    template <typename T>
    T max(T a, T b) {
      std::cout << 1;
      return b < a ? a : b;
    }
    
    template <typename T>
    T max(T a, T b, T c) {
      return max(max(a, b), c);  // 即使对int也使用上述模板,因为没看见下面版本
    }
    
    int max(int a, int b) {
      std::cout << 2;
      return b < a ? a : b;
    }
    
    int main() {
      ::max(3, 1, 2);  // 11
    }
    

    注意事项

    • 在模板中通常传值更好,这样语法简单,编译器优化更好,移动语义可以使拷贝更廉价(有时完全没有拷贝或移动),而且模板可能同时被简单和复杂类型使用,为简单类型选择复杂类型可能适得其反。尽管传字符串字面值常量或者原始数组容易出问题,但给它们传引用通常会造成更大的问题
    • 通常函数模板不需要声明为 inline,唯一例外的是特定类型的全特化。因为编译器可能忽略 inline,函数模板是否内联取决于编译器的优化策略
    • C++11 可以使用 constexpr 函数来生成编译期值
    template <typename T, typename U>
    constexpr auto max(T a, U b) {
      return b < a ? a : b;
    }
    
    int a[::max(sizeof(char), 1000u)];
    std::array<int, ::max(sizeof(char), 1000u)> b;
    

    相关文章

      网友评论

          本文标题:01 函数模板

          本文链接:https://www.haomeiwen.com/subject/bjnvjltx.html