继承std::tuple的派生类无法完成结构化绑定,而std::pair无此问题的原因及解决办法问询
这个问题其实涉及到C++结构化绑定对std::pair和std::tuple的底层处理机制差异,咱们一步步拆解原因和解决办法:
问题根源:结构化绑定的两种实现路径
结构化绑定并不是对所有类都一视同仁,它会根据类的类型选择两种不同的处理逻辑:
- 针对聚合类型:直接访问类的公开非静态数据成员,按顺序绑定到变量上。
std::pair就是这种情况——它是标准定义的聚合类型,拥有first和second两个公开成员,所以当你继承std::pair时,派生类会直接继承这两个成员,结构化绑定可以直接识别并分解它们。 - 针对tuple-like类型:依赖一套标准协议,要求类必须有对应的
std::tuple_size特化、std::tuple_element特化,以及可被调用的std::get重载。std::tuple就是通过这套协议实现结构化绑定的,但标准库只给std::tuple本身做了这些特化,你的派生类s_int_triple并没有享受到这个“待遇”。
另外,你使用using std::tuple<int, int, int>::tuple;继承构造函数的操作,会让你的派生类失去聚合类型的资格(C++17及以后的标准规定,拥有用户声明构造函数的类不属于聚合类型),所以编译器既不能用聚合类型的方式分解成员,又找不到tuple-like协议的支持,自然就报错了。
可行的解决办法
针对这个问题,有三种常用的解决思路,你可以根据自己的需求选择:
办法一:手动为派生类实现tuple-like协议
我们可以为s_int_triple特化标准库的tuple相关模板,并实现std::get的重载,让编译器把它当成tuple来处理:
#include <tuple> #include <utility> using t_int_triple = std::tuple<int, int, int>; struct s_int_triple : public std::tuple<int, int, int> { using std::tuple<int, int, int>::tuple; // 保留继承构造函数 }; // 特化tuple_size,复用std::tuple的定义 namespace std { template<> struct tuple_size<s_int_triple> : tuple_size<t_int_triple> {}; // 特化tuple_element,同样复用基类的定义 template<size_t I> struct tuple_element<I, s_int_triple> : tuple_element<I, t_int_triple> {}; } // 重载get函数,将请求转发给基类的std::get template<size_t I> auto& get(s_int_triple& t) { return std::get<I>(static_cast<t_int_triple&>(t)); } template<size_t I> const auto& get(const s_int_triple& t) { return std::get<I>(static_cast<const t_int_triple&>(t)); } template<size_t I> auto&& get(s_int_triple&& t) { return std::get<I>(static_cast<t_int_triple&&>(t)); } void bar() { auto [a1, a2, a3] = t_int_triple{ 0, 0, 0 }; auto [b1, b2, b3] = s_int_triple{ 0, 0, 0 }; // 现在可以正常编译 }
办法二:将派生类恢复为聚合类型
如果你的场景不需要继承std::tuple的构造函数,可以直接去掉using std::tuple<...>::tuple;这一行。此时在C++17及以后,你的派生类会成为聚合类型(满足聚合类型的所有条件:没有用户声明的构造函数、没有私有/保护的非静态成员、基类是合法的聚合类型),结构化绑定就能直接分解基类的成员了:
#include <tuple> using t_int_triple = std::tuple<int, int, int>; struct s_int_triple : public std::tuple<int, int, int> { // 去掉继承构造函数,恢复聚合类型属性 }; void bar() { auto [a1, a2, a3] = t_int_triple{ 0, 0, 0 }; auto [b1, b2, b3] = s_int_triple{ {0, 0, 0} }; // 注意内层大括号,部分编译器允许省略 }
这种方式的缺点是你不能直接用std::tuple的构造函数语法初始化派生类,需要用聚合初始化的方式。
办法三:添加隐式转换到std::tuple
给派生类添加一个隐式转换运算符,让它可以自动转换为基类的std::tuple类型,这样结构化绑定时会先转换,再分解:
#include <tuple> using t_int_triple = std::tuple<int, int, int>; struct s_int_triple : public std::tuple<int, int, int> { using std::tuple<int, int, int>::tuple; // 保留继承构造函数 // 定义到基类的隐式转换 operator t_int_triple&() { return *this; } operator const t_int_triple&() const { return *this; } }; void bar() { auto [a1, a2, a3] = t_int_triple{ 0, 0, 0 }; auto [b1, b2, b3] = s_int_triple{ 0, 0, 0 }; // 正常编译 }
这种方式最简单,但要注意隐式转换可能会在其他场景带来意外的行为,需要谨慎使用。
内容的提问来源于stack exchange,提问作者Zoppo

