I like abstractions as much as the next person, but oftentimes you can just make due with the exact thing.
I think there is a pro and a con to that approach.
The pro is that there is a meaningful and familiar intermediate representation --- the output of the C pre-processor is still C code. Another example is https://doc.qt.io/qt-5/metaobjects.html
The con is that, well, it introduces a meta layer, and these meta layers are more often than not ad-hoc and ultimately became quite unwieldy. It's a design pattern that suggests that there is a feature missing from the language.
The disadvantage is that making sweeping changes is more work. The advantage is that making sweeping changes can be done incrementally. But the big win with code generators is that all you need to understand what's happening is right in front of you instead of having to mentally unwind a clunky abstraction.
Don't get me wrong if you have a good abstraction that reduces the use of mental registers do it! But you would and should do that regardless of boilerplate.
This is the part where I’ve been entertaining the idea of solving things at a language level (transpiler). It’s a dangerous idea because if we get better tools to extend the language and everyone does it, we’re all going to be fucked.
But I can’t shake the idea that some of our frameworks, if incorporated at a language level, could entirely eliminate the boilerplate.
Maybe that warrants a library, but then you also have to hook that up with the ever so slightly different boilerplate code.
If this 90% of the easy stuff is done for you, that gives you more time to focus on the 10% that matter.