I don't think you need C++20 to do this, but you do need to separate out the arguments you're planning to call your function with if you want to do anything useful. Here's a simple (full) example that I think will do what you want, though I have no idea what you actually plan to do with the tuple you're making:
#include <iostream>
#include <tuple>
#include <string>
#include <functional>
template<typename Func, class... Args>
void DoUniqueCorrectly(Func&& fn, Args&&... args)
{
using ArgsTuple = std::tuple<Args...>;
std::cout << "Getting ready to invoke\n";
ArgsTuple tup(args...);
if constexpr (std::tuple_size<ArgsTuple>{} > 0) {
std::cout << "arg1 = " << std::get<0>(tup);
}
if constexpr (std::tuple_size<ArgsTuple>{} > 1) {
std::cout << ", arg2 = " << std::get<1>(tup);
}
std::cout << '\n';
std::invoke(std::forward<Func>(fn), std::forward<Args>(args)...);
}
void test(const char *arg1, int arg2)
{
std::cout << "test called! arg1 = " << arg1 << ", arg2 = " << arg2 << '\n';
}
int main(int argc, char **argv)
{
DoUniqueCorrectly(test, "hey!", 22);
return 0;
}
Output:
Getting ready to invoke
arg1 = hey!, arg2 = 22
test called! arg1 = hey!, arg2 = 22
Try it yourself on godbolt
The printing of tuple elements is placeholder, to do a proper generic iterator would take up a lot more space. You can find plenty of other questions on stack overflow that do that the "right" way.
Edit: As Ben Voigt points out, the elements of the ArgsTuple are going to have a type which is identical to the types passed to DoUniqueCorrectly, but not necessarily identical to the types of the functional call. A good way to illustrate this is to change the function signature of test() to:
void test(const std::string arg1, int arg2)
Since we invoked DoUniqueCorrectly with a const char *, the ArgsTuple will have type <const char *, int>, and this will be converted to const std::string on the invoke call. This may or may not be important in your particular application, but it's certainly important to be aware of if you adopt a solution like this.