I wrote the following snippet to test if I could perfectly forward values through a tuple and std::invoke
. However the generated assembly looks kind of odd.
#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-46 gcc 12.2 -Wall -Os --std=c++20
Excerpt from line 30:
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> >)
As you can see there's a call to the copy constructor of std::string
. Where does it originate from? My suspicion is the call to foo() using std::get<I>(tuple)
which doesn't seem to perfectly forward the values. In this regard I'm lacking understanding: How is "rvalue-ness" routed through a std::tuple
? Is the value category preserved inside the tuple or is everything converted to either copy or lvalue-reference? Is there a chance to optimize this as well?
Clarification:
My goal is essentially to achieve perfect forwarding from the callsite of async()
to foo()
. That means value categories should be preserved as-is and no unnecessary copies should be made in between.
E.g. When I call async(&foo, std::string("string1"), std::string("string2"))
it should invoke the move constructor for the strings constructed in foo's parameter list.
E.g. When I call async(&foo, str1, str2)
with predefined strings they should be picked up as rvlaue-references instead and foo's parameters should be copy-constructed.