I'd say both Rust and C++ trade blows when it comes to expressiveness. You know Rust already, so I'm not going to try sell you how powerful macros can be (see SQLx's ability to compile-time check SQL queries).
And indeed C++ templates are a lot more like Rust macros than Rust generics: They're turing-complete.
Joint with some interesting language choices like SFINAE (substitution failure is not an error), you end up with the ability to specialize functions, methods and whole classes in C++.
You can also have functions that return different types.
C++ templates work like duck-typing within a static language: In Rust you need to say what traits your generics need to support. In C++ it will try and substitute and if it fails (say, because T doesn't support the required methods) it will try another substitution until none are left.
If none of the substitutions work, you will be shown ALL of them in error reporting: This is what leads to pages and pages of compile errors of single-character typos in C++.
Templates are really cool, but also pretty confusing when reading code since you're in a guessing game of what types will fit the constraints imposed by the _implementation_ of the function.
From C++20 there's concepts to make templates work a little smoother.
There's been whole books written about how to abuse templates: these are pre-requisite knowledge when working in large codebases.
The big one is metaprogramming. Most people that have never really used it grok how powerful (and clean and maintainable) it has become in recent versions of C++. I work on a few different C++20 code bases and the amount of code that is no longer written because it is generated at compile-time with rigorous type safety is brilliant. It goes well beyond vanilla templating, you can essentially build a DSL for the application domain.
Another one, with a more limited audience, is data models where object ownership and lifetimes are inherently indeterminate at compile-time. Since C++ allows you to design your own safety models, since they are opt-in and not built into the compiler, you can provide traditional ownership semantics (e.g. the equivalent of std::unique_ptr) without exposing the mechanics of how ownership or lifetimes are resolved at runtime. Metaprogramming plays a significant role in making this transparent.
Those are the two the matter the most for my purposes. They save an enormous amount of code and bugs. Rust has a litany of other gaps (lack of proper thread local, placement new, et al) but I don't run into those cases routinely.
The data structure thing you mention would be annoying but to be honest I rarely design data structures like this. For performance, most data structures tend to rely on clever abuse of arrays.
And indeed C++ templates are a lot more like Rust macros than Rust generics: They're turing-complete.
Joint with some interesting language choices like SFINAE (substitution failure is not an error), you end up with the ability to specialize functions, methods and whole classes in C++.
You can also have functions that return different types.
C++ templates work like duck-typing within a static language: In Rust you need to say what traits your generics need to support. In C++ it will try and substitute and if it fails (say, because T doesn't support the required methods) it will try another substitution until none are left.
If none of the substitutions work, you will be shown ALL of them in error reporting: This is what leads to pages and pages of compile errors of single-character typos in C++.
Templates are really cool, but also pretty confusing when reading code since you're in a guessing game of what types will fit the constraints imposed by the _implementation_ of the function.
From C++20 there's concepts to make templates work a little smoother.
There's been whole books written about how to abuse templates: these are pre-requisite knowledge when working in large codebases.