tuple内是否保留值类别?通过tuple实现完美转发的问题
完美转发在std::tuple与std::invoke中的实现问题
问题描述
我编写了一段代码,尝试用std::tuple和std::invoke实现值的完美转发,但生成的汇编代码中出现了不必要的拷贝操作。
测试代码
#include <concepts> #include <string> #include <cstdio> #include <tuple> #include <functional> auto foo(std::string str, std::string str2 = "temp") { printf("Foo executed with %s and %s!\n", str.data(), str2.data()); } template <typename Cb, typename... Args> auto async(Cb&& fn, Args&&... args) { constexpr std::size_t cnt = sizeof...(Args); auto tuple = std::make_tuple<Args...>(std::forward<Args>(args)...); std::invoke([&]<std::size_t... I>(std::index_sequence<I...>){ foo(std::get<I>(tuple)...); }, std::make_index_sequence<cnt>{}); } int main() { async(&foo, std::string("mystring"), std::string("other")); }
编译环境
x86-64 gcc 12.2 -Wall -Os --std=c++20
关键汇编片段
call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&&) [complete object constructor] lea rsi, [rsp+16] lea rdi, [rsp+176] call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&&) [complete object constructor] lea rsi, [rsp+144] lea rdi, [rsp+112] call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) [complete object constructor] lea rsi, [rsp+176] lea rdi, [rsp+80] call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) [complete object constructor] lea rsi, [rsp+112] lea rdi, [rsp+80] call foo(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)
疑问与目标
从汇编中可见std::string的拷贝构造函数被调用,我怀疑这源于std::get<I>(tuple)没有实现完美转发,有以下疑问:
- 右值的属性如何通过
std::tuple传递? tuple内部是否保留值类别,还是会将所有值转换为拷贝或左值引用?- 是否有优化空间?
我的目标是实现从async()调用点到foo()的完美转发:
- 调用
async(&foo, std::string("string1"), std::string("string2"))时,foo的参数应触发移动构造函数。 - 调用
async(&foo, str1, str2)(str1、str2为预定义字符串)时,foo的参数应触发拷贝构造函数。
问题根源与解决方案
问题根源
当前实现存在两个核心问题:
std::make_tuple的行为:std::make_tuple<Args...>(std::forward<Args>(args)...)会将传入的右值对象拷贝存储到tuple中——因为当Args推导为右值引用时,std::make_tuple会实例化对应的值类型,而非引用类型,tuple内部存储的是独立对象,而非原对象的引用。std::get的返回值类别:从左值tuple中用std::get<I>(tuple)获取元素时,返回的是左值引用,传递给foo时会触发拷贝构造,而非移动。
修正方案
要实现完美转发,需从两个方面调整:
1. 用std::forward_as_tuple替代std::make_tuple
std::forward_as_tuple会创建一个存储引用的tuple,而非拷贝对象。它会根据传入参数的左/右值属性,分别存储左值引用或右值引用,完整保留原始值类别信息:
auto tuple = std::forward_as_tuple(std::forward<Args>(args)...);
2. 在调用foo时显式完美转发tuple元素
从tuple中取出元素后,需要用std::forward还原其原始值类别。通过std::tuple_element_t<I, decltype(tuple)>获取tuple中第I个元素的原始引用类型,再进行转发:
std::invoke([&]<std::size_t... I>(std::index_sequence<I...>){ foo(std::forward<std::tuple_element_t<I, decltype(tuple)>>(std::get<I>(tuple))...); }, std::make_index_sequence<cnt>{});
完整修正代码
#include <concepts> #include <string> #include <cstdio> #include <tuple> #include <functional> auto foo(std::string str, std::string str2 = "temp") { printf("Foo executed with %s and %s!\n", str.data(), str2.data()); } template <typename Cb, typename... Args> auto async(Cb&& fn, Args&&... args) { constexpr std::size_t cnt = sizeof...(Args); auto tuple = std::forward_as_tuple(std::forward<Args>(args)...); std::invoke([&]<std::size_t... I>(std::index_sequence<I...>){ foo(std::forward<std::tuple_element_t<I, decltype(tuple)>>(std::get<I>(tuple))...); }, std::make_index_sequence<cnt>{}); } int main() { async(&foo, std::string("mystring"), std::string("other")); std::string str1 = "hello", str2 = "world"; async(&foo, str1, str2); }
原理说明
std::forward_as_tuple:模板参数推导为传入参数的引用类型(左值/右值引用),tuple中存储的是对原参数的引用,无拷贝操作。std::forward:通过std::tuple_element_t获取原始引用类型,再用std::forward还原值类别,让foo能根据原始参数类型选择对应的构造函数(移动或拷贝)。
效果验证
修正后的代码编译后,汇编中只会出现对应右值的移动构造函数,以及对应左值的拷贝构造函数,无多余拷贝,完美实现了值类别的传递。
内容的提问来源于stack exchange,提问作者glades
相关产品推荐
相关产品推荐

