Clang-Tidy关于std::tuple{}构造时std::move的警告及构造函数疑问
问题1:Clang-Tidy的警告是否准确?
准确。
当你写return result_t({}, std::move(res));或return result_t(std::move(state.inheritingGroups), {});时,编译器无法匹配std::tuple的可变参数模板构造函数(因为{}是无类型的初始化列表,无法推导模板参数类型)。此时编译器会退而求其次,选择接受const左值引用的构造函数实例——这意味着std::move(res)返回的右值会被绑定到const Result&参数,std::move(state.inheritingGroups)返回的右值会被绑定到const std::unordered_set<QnUuid>&参数。
因为const引用无法触发移动构造(移动构造要求参数是可修改的右值引用),实际执行的是拷贝操作,Clang-Tidy的警告完全正确。
问题2:使用{}作为参数时,std::tuple会选择非模板构造函数是否正确?
不正确。std::tuple没有非模板的多参数构造函数(除了空tuple的默认构造)。
问题的本质是:{}作为初始化列表没有明确类型,导致std::tuple的可变参数模板构造函数(template <class... UTypes> tuple(UTypes&&...))无法推导模板参数,因此被排除在候选列表之外。编译器只能选择其他可行的构造函数——通常是模板构造函数的一个实例化版本,参数为对应元素类型的const左值引用,这才是导致移动失效的原因。
简洁的解决方案
这里有几种不增加代码复杂度的方案,既能消除警告,又保持代码清晰:
方案1:显式构造元素对象,使用括号初始化tuple
直接用显式构造的临时对象替代{},让编译器能匹配到可变参数模板构造函数,从而触发移动:
// 第一个return语句 return result_t(std::unordered_set<QnUuid>{}, std::move(res)); // 第二个return语句 return result_t(std::move(state.inheritingGroups), Result{});
方案2:使用列表初始化构造tuple
改用花括号初始化tuple,编译器会直接将每个初始值对应到tuple的元素,自然支持移动操作:
// 第一个return语句 return result_t{std::unordered_set<QnUuid>{}, std::move(res)}; // 第二个return语句 return result_t{std::move(state.inheritingGroups), Result{}};
方案3:使用std::make_tuple(需显式构造第一个元素)
std::make_tuple能推导右值参数类型,但{}无法推导,所以需要显式构造第一个元素:
// 第一个return语句 return std::make_tuple(std::unordered_set<QnUuid>{}, std::move(res)); // 第二个return语句 return std::make_tuple(std::move(state.inheritingGroups), Result{});
这三种方案都能让std::move真正触发移动构造,消除Clang-Tidy的警告,同时代码简洁易读。
内容的提问来源于stack exchange,提问作者Sergey Kolesnik

