You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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的参数应触发拷贝构造函数。

问题根源与解决方案

问题根源

当前实现存在两个核心问题:

  1. std::make_tuple的行为:std::make_tuple<Args...>(std::forward<Args>(args)...)会将传入的右值对象拷贝存储到tuple中——因为当Args推导为右值引用时,std::make_tuple会实例化对应的值类型,而非引用类型,tuple内部存储的是独立对象,而非原对象的引用。
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 22:25:12