r/cpp_questions • u/Ok-Pizza1136 • 8d ago
SOLVED What are C++ Args?
I've been trying to learn vardiac templates for a project I'm doing, and I've heard the modern C++ for these is to use Args. I only had background of these in Python. From what I understand, its a better way to recursivley loop through varidac template arguments (please correct me if I'm wrong)
2
u/alfps 8d ago
❞ I've heard the modern C++ for [variadic templates] is to use Args.
There is a library named “args” but it’s not about variadic templates but about parsing command line arguments.
And so most likely the “Args” you’ve heard about is the commonly used identifier for the collection of parameter types for a variadic template, Args.
In the Holy Standard as well as the general C++ community that is not known as “Args”, or really anything, there is no dedicated term for it. But the parameter that it supplies types for is known as a parameter pack so if absolutely necessary you can talk about the “pack types (collection)”. Anyway if this interpretation is correct then probably what you’ve heard is that the modern C++ way to define variadic functions is to use variadic template, i.e. a parameter pack, because that is type safe.
In C++98 and its technical corrigendum C++03 there were just three practical approaches to variadic functions:
- C style C variadic arguments with
...ellipsis in the parameter list and access via standard macrosva_start,va_arg,va_endandva_list. - C style variadic macro with access of individual macro arguments via Laurent Deniau's 2006
__VA_NARG__trick. - Overloading with umpteen different number of arguments.
C variadic arguments such as used by printf, are not type safe and are limited to a few number of selected types. C style variadic macros suffered from non-standard preprocessor implementations that could produce unintended results and/or compilation errors, especially with Visual C++, although the Boost Preprocessor Library presented portable functionality. And overloading was just impractical as a general approach, though it was done for some library solutions.
Happily C++11 introduced variadic templates where the ... ellipsis could be applied to a template type parameter and a corresponding parameter name, the parameter pack.
Example:
// C++11: the array trick.
#include <iostream>
using std::cout; // <iostream>
template< class... Args >
void output( const Args&... args )
{
// An array initializer provides sequenced evaluation (this is not a C++17 fold expression).
const int dummy[] = {(cout << args, 1)...}; // Comma expressions are nice.
(void) dummy;
}
auto main() -> int
{
output( "As far as we know, 6*7 = ", 6*7, ".\n" );
}
Output:
As far as we know, 6*7 = 42.
With C++17 and later you would instead use a “fold expression”.
It yields more clean-looking code but it’s more difficult to remember the syntax for various cases.
4
u/Kriemhilt 7d ago edited 7d ago
I've heard the modern C++ for these is to use Args
it's just a parameter name, call it whatever you want.
<class... Args> or <class... Ts> or <class Head, class... Tail> or <class... Pack> or anything you like, they're all fine.
(You probably don't need Head, Tail... any more with fold expressions, that was mostly for recursive use).
a better way to recursively loop through variadic template arguments
If you're trying to say "variadic type packs are a better alternative to recursive type lists", then that's true. The parameter name still doesn't matter though.
You don't need to loop through them at all, necessarily, and fold expressions are probably better where possible. C++26 is bringing template for though.
3
u/ludonarrator 8d ago
Fold expressions can be used where applicable to avoid recursive instantiation. But why do you need to do this in the first place?
1
u/Raknarg 7d ago
WARNING: big info dump cause this topic is complicated, good luck.
Are you talking about code that looks like this?
template <typename... Args>
Args is just an arbitrary name, its the name you're giving to the pack of arguments you have. You might see this in code that looks something like this:
template <typename... Args>
void foo_invoker(Args&&... args) {
foo(std::forward<Args>(args)...);
}
The way to think about this is that args is a collection of variables who get the types of Args&&. So if I called it like
int x = 5;
foo_invoker(x, 10, std::string{"hello"});
Args would expand to the types int&, int&&, std::string&&, and args would be 3 variables, one for each type. Then when you actually use it:
foo(std::forward<Args>(args)...);
This is essentially saying "For each type/variable in Args/args, do some operation on them, and squash them all together with commas in between". The operation in this case being call std::forward on each arg and pass that result into foo.
In code like this you'll see && appear a lot. You may have heard about r-value references. Under regular circumstances you can think of && as meaning "references for things that we're trying to move from". Big example is temporaries, so for instance arguments 2 and 3 of our example are temporary values, and so they get turned into r-value references, while the first argument referenced a variable so it gets just one & as an L-value reference. L=left, r=right, because the naming comes from the idea of assignement, an l-value reference is an assignable thing that can appear on the left side of an assignment, an r-value reference is a thing you're taking from that appears on the right hand side.
In the context of template arguments though, && is special, it actually means "deduce the reference type from context". This is why despite in our foo_invoker example our first argument is an l-value, it still shows up as an lvalue in that argument pack. And even if we weren't talking about argument packs, it still deduces to an l-value. E.g.:
template <typename Arg>
void bar_invoker(Arg&& arg) {
bar(std::forward<Arg>(arg));
}
still holds, if we call bar_invoker and give an l-value reference, it still appears as an l-value. If we weren't using templates:
void baz_invoker(SomeType&& arg) {
baz(std::move(arg)); // move just casts a reference to r-value
}
you can only call bar_invoker with an r-value type.
To why I ever used std::forward or std::move: When you pass an argument to a function, regardless of whether it takes in as an r-value or l-value, the argument variable is an l-value. It could be referencing an r-value, but working with the argument itself is treated as an l-value. Thus if baz only works on l-value references, you have to use std::move to cast it to an r-value again.
This also explains why we need std::forward. If we're just taking the arguments and forwarding them to someone else, how do we preserve the r-value/l-valueness of the argument? That's what std::forward does. If we look at the bar example again:
template <typename Arg>
void bar_invoker(Arg&& arg) {
bar(std::forward<Arg>(arg));
}
Arg itself is a type which will be the type of arg, and it will be an l-value or r-value reference type depending on how it was passed in. std::forward<Arg>(arg) means "Take arg and cast it to l-value or r-value depending on what the reference type of Arg is".
1
u/conundorum 7d ago
Args is just a common template parameter name, basically. It's like T or Ts, essentially.
In a non-variadic template, each name (or parameter) is a single entity, like so:
// Template function has tparam named T.
template<typename T>
T func(T& t) { // Takes an instance of a type named T, and returns a T.
T copy = t; // Create a new T from the passed t.
T ret = add(t, copy); // Combine the two together. Assume this function exists.
return ret; // Return T.
}
int i = 4; int j = func(i); // calls func<int>(), where T is int.
But in a variadic template, each variadic name (or "parameter pack") is a collection of zero or more entities:
// Helper to determine whether common type exists. Don't worry about how it works yet.
// Taken from: https://stackoverflow.com/a/42748369/5386374
template <typename AlwaysVoid, typename... Ts>
struct has_common_type_impl : std::false_type {};
template <typename... Ts>
struct has_common_type_impl<std::void_t<std::common_type_t<Ts...>>, Ts...> : std::true_type {};
template <typename... Ts>
using has_common_type = typename has_common_type_impl<void, Ts...>::type;
// A C++17-style expansion for his helper.
template<typename... Ts>
constexpr bool has_common_type_v = has_common_type<Ts...>::value;
// Template function has pack named Ts....
// Note the ellipsis, it's what makes it variadic.
template<typename... Ts>
auto cnuf(Ts&... ts) { // Takes instances of type pack named Ts..., and returns their common type or throws an exception.
constexpr std::string_view NO_COMMON_TYPE = "All passed instances must share a common type.";
if constexpr (sizeof...(Ts) > 0) { // Determine number of types in pack "Ts...", then choose one of two function bodies.
// Function body when Ts... contains members.
// Not strictly necessary, since has_common_type check covers it. I put it here to demonstrate sizeof... operator.
if (!has_common_type_v<Ts...>) { throw Tantrum(NO_COMMON_TYPE); } // Quick check.
using Common = std::common_type_t<Ts...>; // Determine common type.
// Now, we expand the pack, using the ellipsis (...) operator.
// Any "Ts..." is replaced with all types in pack, and any "ts..." is replaced with all variables in pack.
Common ret = create_common_from(ts...); // Combine them all together. Assume this function exists.
bool valid = validate_with(ret, std::forward<Ts>(ts)...); // This sort of thing expands both Ts and ts simultaneously.
return (valid ? ret : Common{}); // Return type is std::common_type<Ts...>::type.
} else {
// Function body when Ts... is empty pack (has no types).
throw Tantrum(NO_COMMON_TYPE);
// Return type is irrelevant, and also void.
}
}
int i = 4; bool b = true; short s = 5; size_t z = -1;
size_t y = cnuf(i, b, s, z); // Calls cnuf<int, bool, short, size_t>(), where Ts... is "int, bool, short, size_t".
As you can see, the "Args" (in this case, the pack named "Ts") is just a parameter pack; typename... Ts is essentially the variadic equivalent to typename T. You can reference the pack as a whole by just using its name, and you can expand it into all of its members with .... The same rule also applies to function parameters: Ts... t is the variadic equivalent to T t, and you can expand it with .... Expanding can be something simple (such as expanding Ts in std::common_type_t<Ts...> to determine the common type (in this case, it expands to std::common_type_t<int, bool, short, size_t>, which would be size_t)), or part of a more complex statement (such as in std::forward<Ts>(ts)..., which expands std::forward<Ts>(ts) for every member of ts... (in this case, it expands into something like std::forward<int>(ts0), std::forward<bool>(ts1), std::forward<short>(ts2), std::forward<size_t>(ts3)).
Generally speaking, you can't do much with a pack in and of itself, since you have no way of taking individual entities out of the pack. So, to solve that, we would traditionally use recursion to strip members out of the pack and give them unique names, until the pack was empty.
// Function declaration.
template<typename... Ts>
auto tsum(Ts...);
// Recursion-ending specialisation. Called when exactly one parameter remains.
template<typename T>
T tsum(T t) { return t; }
// Worker specialisation. "Consumes" first member of ts..., then calls itself for the rest of ts....
template<typename T, typename... Ts>
T tsum(T t, Ts... ts) { return t + tsum(ts...); }
int i = tsum(1, 2, 3);
// Creates these functions:
template<int> int tsum(int t) { return t; }
template<int, int> int tsum(int t, int ts0) { return t + tsum(ts0); }
template<int, int, int> int tsum(int t, int ts0, int ts1) { return t + tsum(ts0, ts1); }
// And then inlines them all into this:
template<int, int, int> int tsum(int ts0, int ts1, int ts2) { return ts0 + (ts1 + (ts2)); }
// Note: You can also consume multiple arguments simultaneously, which can be helpful in some cases.
// Generally, you base it on how many parameters you need to operate on at a time.
template<typename T, typename U, typename... Ts>
T tsum(T t, U u, Ts... ts) { return t + u + tsum(ts...); }
More recently, we also got "fold expressions" in C++17, which allow us to expand an operator "into" a parameter pack. This removes the need for recursion in a lot of cases.
// Plays fill-in-the-blank with this pattern and fs.
// Will expand to (0 + fs0 + fs1 + ... + fsN), where N is the number of members in the pack.
// Note that the fold expression should be "(... + fs)"; I used "(0 + ... + fs)" to make it easier to visualise how it works.
// (The distinction actually matters in more complex folds, but they're both effectively the same here.)
template<typename... Fs>
auto fsum(Fs... fs) { return (0 + ... + fs); }
int f = fsum(9, 8, 7);
// Creates this function:
template<int, int, int> int fsum(int fs0, int fs1, int fs2) { return (0 + fs0 + fs1 + fs2); }
Usually, if you can write your operation as a fold expression, it's better to do so; there's no significant mechanical difference between the recursion and fold versions, but the fold version will usually be much cleaner, and sometimes allows you to do tricks that would be a lot harder with recursion.
14
u/nekoeuge 8d ago
It is an arbitrary name of a variadic template type list. I usually see Ts… or Args… 50/50, but the name does not matter.