使用带约束的模板父类成员函数
带约束的模板父类成员函数
嘿,我瞅了瞅你这段代码,这是想搞一个能按索引访问不同类型成员的组合类对吧?咱们来拆解下这段代码的逻辑,顺便聊聊需要注意的点:
首先先把补全后的完整代码放出来(你原来的多参数偏特化版本没写完,我帮你补了关键部分):
#include <cstddef> template <size_t I, typename... T> class CombineI; // 递归终止:仅单个类型的情况 template <size_t I, typename T> class CombineI<I, T> { public: // 带编译期约束的get函数:只有调用时的i等于类模板的I才能用 template <size_t i> requires(i == I) const T &get() const { return data; } // 可选:非const版本的get,支持修改成员 template <size_t i> requires(i == I) T &get() { return data; } private: T data; }; // 递归展开:多类型参数的情况,继承自索引+1的剩余参数组合类 template <size_t I, typename T, typename... R> class CombineI<I, T, R...> : public CombineI<I + 1, R...> { public: // 当前索引I对应的get重载 template <size_t i> requires(i == I) const T &get() const { return data; } // 可选:非const版本 template <size_t i> requires(i == I) T &get() { return data; } // 构造函数:初始化当前成员和父类成员 template <typename U, typename... Args> CombineI(U&& val, Args&&... args) : CombineI<I + 1, R...>(std::forward<Args>(args)...), data(std::forward<U>(val)) {} private: T data; };
接下来给你掰扯下核心逻辑:
- 递归终止特化:当
CombineI的类型参数包只剩一个类型时,这个类就是递归的终点,只持有对应类型的成员,并且用requires(i == I)做编译期检查——只有调用get<i>()的i和类模板的I完全匹配,才能访问这个成员,从根源上避免了索引越界或者类型不匹配的问题。 - 递归展开偏特化:当有多个类型参数时,这个类会继承自
CombineI<I+1, R...>,相当于把索引往后推一位,处理剩下的类型。这里必须给当前索引I也加一个get重载,不然只能访问父类里的成员,没法拿到当前层的data。 - 编译期约束的优势:C++20的
requires约束比SFINAE写法更直观,编译错误提示也更友好,比如你要是调用了不存在的索引,编译器会直接告诉你没有匹配的get函数,而不是一堆晦涩的模板推导错误。
给你整个使用的小例子:
#include <iostream> #include <string> int main() { CombineI<0, int, std::string, double> comb{42, "Hello World", 3.1415}; // 正确访问各个索引的成员 std::cout << comb.get<0>() << std::endl; // 输出42 std::cout << comb.get<1>() << std::endl; // 输出Hello World std::cout << comb.get<2>() << std::endl; // 输出3.1415 // 尝试修改成员(需要非const的get重载) comb.get<0>() = 100; std::cout << comb.get<0>() << std::endl; // 输出100 // 下面这行编译会报错,因为没有索引3的成员 // std::cout << comb.get<3>() << std::endl; }
最后提两个小建议:
- 如果你想让这个类更易用,可以加一个
make_combine辅助函数,自动推导起始索引为0,不用手动写CombineI<0, ...>。 - 可以考虑添加
value_type、size等类型别名,和标准库容器的接口对齐,用起来更顺手。
内容来源于stack exchange
相关产品推荐
相关产品推荐

