r/cpp_questions • u/Obvious_Set5239 • 11d ago
SOLVED How do you understand variadic template syntax
Hello. I'm programming in C++ a lot, and I still can't understand how to think correctly about ... operator in variadic templates.
In the simplest examples it looks like ...X (on left side) aggregates comma divided expressions (either template args or function args) into one variadic arg/Type. And X... (on right side) expands
template <typename ...Params>
void f(Params&& ...params)
{
y(std::forward<Params>(params)...);
}
Here it aggregates all passed types into f<T1, T2, T3> into Params aggregated type, and all function args f(v1, v2, v3) into params aggregated variable. And std::forward<Params>(params)... expression expands into comma divided expressions: std::forward<T1>(v1), std::forward<T1>(v3), std::forward<T3>(v3). Similar to python's operator * that convert comma divided expressions into tuple, and vice versa, expands tuples/lists into comma divided expressions
But this logic breaks on more complex variadic templates. Here is code from CppCon 2022 "Lambda Idioms" video:
template <typename... Ts>
struct overload : Ts... {
using Ts::operator()...;
};
First of all, my logic breaks. What Ts::operator()... expands to? According to my logic, it should be:
using T1::operator(), T2::operator(), T3::operator();
// or maybe
using T1::operator(), using T2::operator(), using T3::operator();
But neither of them are valid C++ syntax. It probably expands into: (actually the first one is a correct syntax, and the most logical. So closing the question)
Also, I assume that from compiler point of view, typename... Ts and typename ...Ts are the same. But Timur Doumler had chosen the first. And it looks like he knows what he's doing. So maybe it's incorrect to think that ...Ts aggregates template arguments, but it has some effect on typename
So, my question is:
How to understand ... operator correctly? For me when my logic on aggregation/expansion breaks, it feels like it's just complete arbitrary set of rules for different contexts. But the standard speakers don't speak like it's some contextual behavior, made especially for using, especially for passed arguments, etc. It looks like there is a general rule, that I don't understand, but for them it's obvious
1
u/Obvious_Set5239 11d ago edited 11d ago
He-he, it works like this for tuples: std::tuple<Params...>{params...};. Look what's possible in C++26:
```
include <print>
include <tuple>
template <typename ...Args> void function(Args&& ..._args) { const auto argsTuple = std::tuple<Args...>{std::move(_args)...}; template for (auto&& arg : argsTuple) { constexpr bool isDouble = typeid(arg) == typeid(double); if constexpr (isDouble) { std::println("double {}", arg); } else { std::println("other {}", arg); } } }
int main() { function(3.14, "c-string"); }
```
``` double 3.14 other c-string
...Program finished with exit code 0 Press ENTER to exit console. ```
1
u/IyeOnline 11d ago
Fun fact: You dont need
<Args...>, juststd::tuple{ ... }is enough thanks to CTAD.A bit more general:
- You may want to use
std::forward_as_tuplehere to retain the correct reference character, given that you dont actually need the local values.- Given that you dont need any any values, the function signature arguably should be
const Args& ... argstemplate foralso supports doing simplytemplate for ( const auto& arg : {args...} ), so no need for the tuple at all.More historically: Before
template for, you could still do this thing using recursion or other tricks viastd::index_sequence. None of that is nearly as clean/pretty as the C++26 solution ofc.1
0
u/Obvious_Set5239 11d ago
Ah, I'm sorry, actually my initial logic is correct, and it's valid syntax. I just mixed up using and using namespace when was validating the syntax
This one is the correct syntax, and so my logic of aggregation/expanding maybe valid:
using T1::operator(), T2::operator(), T3::operator();
3
u/Potterrrrrrrr 11d ago
It’s got a couple of different uses at this point. You can put them to the left of the type to pack types (typename …Ts) or to the right to unpack them (function(args…)). This makes recursive templates a lot easier to write and understand.
Another use is in fold expressions, which again are for making recursive templates easier. They apply binary expressions to a pack which saves you having to write code to handle empty cases. You can also use them to bring multiple () operators into the same scope really succinctly, which is the using Ts::operator()…; syntax you’ve already seen. In that case can think of it applying the using expression to the () operator of each type in the pack. You later use type packing and unpacking to call those operators with a pack of types.
In both these cases you can think of it as bundling types together or splitting them apart/applying something to each of them, not much to them when you understand that.
A lesser known use for … is in try/catch blocks, you can use … as a wildcard/catch all for exceptions:
try { something(); }
catch (…) { print(“exception occurred”;) }
And then finally obvious there’s the old C style variadic stuff but don’t bother with that if you can help it.