C++17元组推导指南(CTAD):隐式生成与用户自定义探究
代码示例
#include <utility> template <class... Types> class my_tuple { public: // constexpr my_tuple() {} // explicit my_tuple(const Types&...){} template <class... UTypes> explicit my_tuple(UTypes&&...u){} template <class U1, class U2> my_tuple(const std::pair<U1, U2>& u) {} }; // User-Defined Deduction Guides // template<class... UTypes> // my_tuple(UTypes...) -> my_tuple<UTypes...>; // #1 // template<class T1, class T2> // my_tuple(std::pair<T1, T2>) -> my_tuple<T1, T2>; // #2 int main(){ int i = 1; double d = 4.5; auto mtp1 = my_tuple(i, d); // CTAD #1 const auto p = std::pair(i, d); auto mtp2 = my_tuple(p); // CTAD #2 }
问题现象
- 仅启用上述两个构造函数时,类模板实参推导(CTAD)可正常工作,推测使用的是隐式生成的推导指南;
- 启用全部四个构造函数后,GCC和CLANG均报“multiple overloads instantiate to the same signature”编译错误;
- 此时启用用户自定义推导指南则恢复正常。
核心疑问
- 为何初始状态下隐式推导指南可正常工作,而启用全部构造函数后必须使用用户自定义推导指南?
- mtp1和mtp2的构造过程是依赖用户自定义推导指南,还是实际使用了隐式生成的推导指南?
另外,根据CPPreference说明,std::tuple的库推导指南仅用于处理隐式推导遗漏的边缘情况(如不可复制参数、数组转指针转换),而本例中的两个构造场景似乎不属于此类边缘情况。
个人推测的初始状态隐式推导指南
template<class... UTypes> my_tuple(UTypes&&...) -> my_tuple<UTypes&&...>; // #1 template<class U1, class U2> my_tuple(const std::pair<U1, U2>&) -> my_tuple<const std::pair<U1, U2>&>; // #2 template <typename T> my_tuple(my_tuple<T>) -> my_tuple<T> // #3
一、初始状态隐式推导指南能工作的原因
初始状态下只有两个构造函数,编译器为它们生成的隐式推导指南签名无冲突:
- 可变参数转发构造函数对应的推导指南是
template<class... UTypes> my_tuple(UTypes&&...) -> my_tuple<UTypes&&...>,针对任意数量的转发参数; - 接受
const std::pair<U1,U2>&的构造函数对应的推导指南是template<class U1, class U2> my_tuple(const std::pair<U1, U2>&) -> my_tuple<const std::pair<U1, U2>&>,参数数量固定为1,和第一个推导指南的匹配场景不重叠。
对于mtp1 = my_tuple(i,d),编译器匹配第一个隐式推导指南,推导UTypes为int&, double&(左值转发引用推导为左值引用),最终my_tuple的模板参数是int&, double&,推导过程合法;对于mtp2 = my_tuple(p),匹配第二个隐式推导指南,推导得到my_tuple<const std::pair<int,double>&>,同样能正常完成推导。
二、启用全部构造函数后报错的原因
启用另外两个构造函数后,问题出在接受const Types&...的构造函数和转发构造函数生成的隐式推导指南上:
- 转发构造函数的隐式推导指南是
template<class... UTypes> my_tuple(UTypes&&...) -> my_tuple<UTypes&&...>; const Types&...构造函数对应的隐式推导指南是template<class... UTypes> my_tuple(const UTypes&...) -> my_tuple<UTypes...>。
当传入左值(比如i和d)时,这两个推导指南会实例化出签名完全相同的候选函数,编译器无法区分,从而触发“multiple overloads instantiate to the same signature”错误。
三、mtp1和mtp2的构造依赖哪种推导指南
- 初始状态(仅两个构造函数)时,完全依赖隐式生成的推导指南:mtp1匹配转发构造函数的隐式指南,mtp2匹配
std::pair构造函数的隐式指南; - 启用全部构造函数后,隐式指南出现冲突,必须用用户自定义推导指南覆盖:自定义指南
my_tuple(UTypes...) -> my_tuple<UTypes...>会明确推导模板参数为UTypes的原始类型(而非引用),my_tuple(std::pair<T1,T2>) -> my_tuple<T1,T2>会将std::pair的成员类型作为my_tuple的模板参数,既解决了冲突,又符合std::tuple的预期行为。
四、关于std::tuple推导指南的补充
你提到的CPPreference说明没错,std::tuple的库推导指南确实是补全隐式推导的短板:比如隐式推导会把数组参数推导成指针,库指南会保留数组类型;对于不可复制的右值参数,隐式推导可能出问题,库指南能正确处理。你的例子中初始隐式推导虽能工作,但推导结果(比如my_tuple<int&, double&>)和std::tuple的预期(my_tuple<int, double>)不一致,这也是std::tuple需要自定义推导指南的原因之一。
内容的提问来源于stack exchange,提问作者J L

