1.2、模板模板参数改进

一、什么是模板模板参数?为什么C++17要改进它?

先回顾一下:模板模板参数就是模板的参数是另一个模板。比如:

template<typename T, template<typenametypenameclass Cont>
class MyContainer {
    Cont<T, std::allocator<T>> data;
    // ...
};

这里Cont是一个模板模板参数,要求传入的模板必须是一个带两个类型模板参数的类模板,比如std::vector

在C++14及之前,模板模板参数声明时只能用class关键字,而且模板参数的匹配非常严格:传入的模板参数列表必须完全匹配声明的模板参数列表,否则编译失败。

C++17改进点

  • • 允许用typename替代class声明模板模板参数
    以前只能写template <typename T, template<class, class> class Cont>,现在可以写成template <typename T, template<typename, typename> typename Cont>,语义更统一,代码更清晰。
  • • 模板模板参数匹配更宽松,支持默认模板参数匹配
    之前如果模板模板参数声明的是两个模板参数,但传入的模板带有默认参数,匹配会失败。C++17修正了这个问题,允许带默认模板参数的模板匹配模板模板参数,即使默认参数没有显式写出,也能匹配成功。

二、设计哲学与底层原理剖析

设计哲学:让模板匹配更灵活,减少模板使用障碍

  • • 统一语法:typenameclass在模板类型参数中本质等价,C++17允许模板模板参数也用typename,统一了语言风格,减少认知负担。
  • • 匹配宽松:模板模板参数匹配规则放宽,支持默认模板参数的匹配,减少模板参数书写的繁琐,提升模板代码的复用性。
  • • 兼容性提升:解决了旧标准下模板模板参数匹配失败的问题,避免了大量模板代码因默认参数而无法匹配的尴尬。

底层原理:模板参数匹配规则的调整

模板模板参数匹配的本质是“模板模板参数列表”和“实参模板参数列表”的形状(参数个数和类型)是否兼容。C++17允许:

  • • 实参模板参数列表可以多于模板模板参数声明的参数个数,只要多出的参数都有默认值。
    这样,模板模板参数声明template<typename, typename>可以匹配实参模板template<typename, typename=std::allocator<T>>

这背后是标准委员会对模板匹配规则的细化和放宽,兼顾了灵活性和类型安全。

三、深度案例解析

案例1:用typename替代class声明模板模板参数

#include <vector>
#include <list>
#include <iostream>

template<typename T, template<typenametypenametypename Cont>
class MyContainer {
public:
    MyContainer(std::initializer_list<T> il) : data(il) {}

    void print() const {
        for (const auto& elem : data) {
            std::cout << elem << " ";
        }
        std::cout << std::endl;
    }
private:
    Cont<T, std::allocator<T>> data;
};

int main() {
    MyContainer<int, std::vector> vecCont{12345};
    vecCont.print();

    MyContainer<std::string, std::list> listCont{"a""b""c"};
    listCont.print();

    return 0;
}

解析:

  • • 这里Conttemplate<typename, typename> typename Cont声明,替代了传统的class关键字,语义完全等价。
  • • std::vectorstd::list都带两个模板参数(元素类型和分配器),符合模板模板参数要求。
  • • 代码简洁且语义清晰,体现了C++17对模板模板参数声明的语法改进。

案例2:默认模板参数匹配的灵活性

#include <vector>
#include <iostream>

template<typename T, template<typenametypename = std::allocator<T>> class Cont>
class Wrapper {
public:
    Wrapper(std::initializer_list<T> il) : data(il) {}

    void print() const {
        for (const auto& elem : data) {
            std::cout << elem << " ";
        }
        std::cout << std::endl;
    }
private:
    Cont<T> data;
};

int main() {
    Wrapper<int, std::vector> w{102030};
    w.print();
    return 0;
}

解析:

  • • Wrapper模板模板参数声明了第二个模板参数带默认值= std::allocator<T>
  • • C++17允许传入的模板std::vector带默认模板参数匹配成功。
  • • 这在C++14中会编译失败,因为模板模板参数和实参模板参数列表不完全匹配。
  • • 这里Cont<T>实际展开为std::vector<T, std::allocator<T>>,使用了默认参数。

案例3:C++17前后匹配差异导致的歧义问题

#include <vector>
#include <iostream>

template<typename T, typename Alloc>
void func(const std::vector<T, Alloc>&) {
    std::cout << "func with std::vector<T, Alloc>" << std::endl;
}

template<typename T, template<typenameclass Vector>
void func(const Vector<T>&) {
    std::cout << "func with Vector<T>" << std::endl;
}

int main() {
    std::vector<int> v;
    func(v); // C++14编译通过,C++17编译报错:调用歧义
    return 0;
}

解析:

  • • C++14中,std::vector有两个模板参数,第二个带默认值,模板模板参数template<typename> class Vector不匹配,调用第一个func
  • • C++17中,默认模板参数匹配规则放宽,两个func都成为候选,导致调用歧义。
  • • 这体现了模板模板参数匹配规则改进带来的副作用,提醒我们要注意重载设计。

四、常见错误与注意事项

  • • 误用classtypename
    C++17允许模板模板参数用typename替代class,但混用时需保持一致,避免代码风格混乱。
  • • 默认模板参数匹配导致的歧义
    如案例3所示,默认参数匹配可能引发重载歧义,设计函数模板时应避免模糊匹配。
  • • 模板模板参数参数列表不匹配
    传入模板参数列表与模板模板参数声明不兼容仍然会编译失败,尤其是非默认参数部分。
  • • 对非类型模板参数的限制
    模板模板参数只能匹配模板类型参数,不能匹配非类型模板参数或模板模板参数本身。

五、总结与独到观点

C++17对模板模板参数的改进,虽是细节,却极大提升了模板编程的灵活性和一致性。它让模板模板参数的声明更统一,匹配更宽松,减少了历史遗留的繁琐限制。
本文首发于【讳疾忌医-note】公众号,未经授权,不得转载。
(加入我的知识星球,免费获取账号,解锁所有文章。)

 

阅读剩余
THE END