C++ Template meta-progamming - parsing lists of numbers - Page 1
Forum section: electronics / general

author: westfw
author_rank: Super Contributor
timestamp: April 03, 2025, 06:36:04 am
content: I'd like to pass a C++ template function a string containing a list of numbers, and have it parsed at compile time into an array of bytes that ends up being passed to actual run-time code.Ie:Code:set_many_pins("1 10 8 5 2");// calls a worker functionset_many_pins_worker(5); // 5 values stored in a static array somewhere.My knowledge of C++ template programming doesn't go much further than believing that this should be possible...Any hints or examples? C++11 would be desirable :-(

author: Tation
author_rank: Regular Contributor
timestamp: April 03, 2025, 08:13:26 am
content: Wouldn't beconstexprmore appropriate for this task? Definitely not an expert here, but used it to create some non-trivial data structures (mainly LUTs) at compile time.

author: brucehoult
author_rank: Super Contributor
timestamp: April 03, 2025, 08:35:48 am
content: Quote from: westfw on April 03, 2025, 06:36:04 amSir, it is already the 3rd.

author: tggzzz
author_rank: Super Contributor
timestamp: April 03, 2025, 08:36:06 am
content: Quote from: westfw on April 03, 2025, 06:36:04 amSince the template language is Turing complete, it should indeed be possible to do that. You can, never forget, cause the C++ compiler to emit the never-ending sequence of prime numbersduring compilation.What you should and shouldn't do in the template language is a matter of taste.

author: SiliconWizard
author_rank: Super Contributor
timestamp: April 03, 2025, 02:22:10 pm
content: You can do that using variadic arguments instead of messing with templates?If you're allergic to messing with variadic arguments, you can alternatively use std::initializer_list (since C++11).Unless you prefer templates and strings to lists within curly braces, or absolutely need to use strings at compile-time for a reason I don't know, this is easier, easier to understand and more elegant IMO. Example:Code:#include <cstdint>#include <initializer_list>#define PORT((volatile uint32_t *) 0xABCD1234)static void set_pin(uint8_t pin){*PORT |= pin;}static void set_many_pins(std::initializer_list<uint8_t> pinList){for (uint8_t pin : pinList){set_pin(pin);}}void Foo(){set_many_pins({ 1, 10, 8, 5, 2 });}(The set_pin() function is obviously hypothetical just to illustrate.)This is what you get from the above for a Cortex-M4 at -O3:Code:Foo():ldr r3, .L3ldr r2, [r3, #564]orr r2, r2, #1str r2, [r3, #564]ldr r2, [r3, #564]orr r2, r2, #10str r2, [r3, #564]ldr r2, [r3, #564]orr r2, r2, #8str r2, [r3, #564]ldr r2, [r3, #564]orr r2, r2, #5str r2, [r3, #564]ldr r2, [r3, #564]orr r2, r2, #2str r2, [r3, #564]bx lr.L3:.word -1412624384Directly inlined.At -Os, it uses an array and a loop instead:Code:Foo():push {r4, lr}ldr r1, .L4ldr r0, .L4+4movs r3, #6.L2:subs r3, r3, #1bne .L3pop {r4, pc}.L3:ldr r4, [r0, #564]ldrb r2, , #1 @ zero_extendqisi2orrs r2, r2, r4str r2, [r0, #564]b .L2.L4:.word .LANCHOR0.word -1412624384.set .LANCHOR0,. + 0C.0.0:.byte 1.byte 10.byte 8.byte 5.byte 2

author: westfw
author_rank: Super Contributor
timestamp: April 04, 2025, 02:42:40 am
content: QuoteThanks! I think that accomplishes what I want to do.I had previously found something similar, but it required a more recent version of C++, which I don't have.

author: SiliconWizard
author_rank: Super Contributor
timestamp: April 04, 2025, 12:48:26 pm
content: This above is supported in C++ 11, tested compiling it with 'std=c++11' to be sure - this is what gives the assembly I posted. It's pretty efficient too (I don't think one could get any more efficient) and easy to use.One of the C++ things that can be "safely" used in embedded dev without a second thought.

author: bson
author_rank: Unknown
timestamp: July 19, 2025, 09:02:22 pm
content: Of course, you can just use an array instead of the initializer_list.Code:static const int pins[] = { 1, 10, 8, 5, 2 };set_many_pins(pins, sizeof pins);That's pretty much all initializer_list is, a reference to an array and its size, with iterators. It's effectively an immutable vector with fixed underlying storage.I find these standard library wrappers pretty useless, it's not like it's a challenge to walk an array, it doesn't need an abstraction; nor does an abstraction help readability - but drags in all kinds of abstract silliness like iterators.

author: gf
author_rank: Super Contributor
timestamp: July 20, 2025, 12:38:34 pm
content: Quote from: bson on July 19, 2025, 09:02:22 pmThe obvious idea is that you can pass {1, 10, 8, 5, 2} directly as an argument, without having to instantiate an array explicitly. The original intended use case was certainly to initialise class objects using syntax similar to that used for arrays or structs, so that you can write directlyCode:std::vector<int> v1 = { 1, 10, 8, 5, 2 };instead ofCode:static const int pins[] = { 1, 10, 8, 5, 2 };std::vector<int> v2(pins, pins + 5);

author: ejeffrey
author_rank: Super Contributor
timestamp: July 20, 2025, 04:25:48 pm
content: Quote from: bson on July 19, 2025, 09:02:22 pmBy using the provided iterators or list size you don't have to convert sizeof(pins) from byte count to element count. Or assign to a temporary variable. Or remember to declare the temporary variable static const. It's not a huge inconvenience but it's a very common use case, and the initializer_list imis basically a compile time construct it doesn't add any runtime overhead. I don't understand why anyone would object if they are already using C++ in any capacity.Probably the biggest reason for using initializer_list is when initializing member variables within a constructor. This makes it much easier to avoid default initialization followed by assignment.

author: bson
author_rank: Unknown
timestamp: July 20, 2025, 08:57:51 pm
content: Quote from: gf on July 20, 2025, 12:38:34 pmBut only to a constructor. That's why it's calledinitializer_list. The example given is a function call, not a constructor. So you still need to construct it first, even if as an inline temporary. And passing initializer_list's to a function call that doesn't actually do initialization is one of those things that scream "just because you can doesn't make it a good idea."

author: gf
author_rank: Super Contributor
timestamp: July 20, 2025, 09:22:54 pm
content: Quote from: bson on July 20, 2025, 08:57:51 pmNot only for a constructor. Seehttps://en.cppreference.com/w/cpp/utility/initializer_list.htmlQuote

author: TheCalligrapher
author_rank: Regular Contributor
timestamp: July 21, 2025, 05:35:13 am
content: Quote from: westfw on April 03, 2025, 06:36:04 amIf this is intended to be a compile-time construct, why do you want to use astringliteral specifically? Is it really a requirement?

author: TheCalligrapher
author_rank: Regular Contributor
timestamp: July 21, 2025, 05:41:38 am
content: Quote from: ejeffrey on July 20, 2025, 04:25:48 pmUm... This strange sentence seems to make no sense (unless it is intended to be some sort of language-lawyer-level joke)It is indeed very useful to useconstructor initializer listto initialize class bases and members in a constructor. This is an important language feature present in C++ since the beginning of times (C++98). However, this feature has absolutely no relation whatsoever to `std::initializer_list<>`. The latter is a completely independent and unrelated feature introduced in C++11.The names of these features use similar sub-tokens "initializer" and "list", bit otherwise they are completely independent. I don't see why you'd decide to mention constructor initializer lists in a conversation about `std::initializer_list<>`

author: ejeffrey
author_rank: Super Contributor
timestamp: July 21, 2025, 02:39:02 pm
content: Quote from: TheCalligrapher on July 21, 2025, 05:41:38 amLanguage lawyer "jokes" that are annoying, condescending, and don't actually help anyone is more your thing.In this case, try thinking before you start insulting people.The point is: if you have a std::vector or another container as a class member. If you want to initialize it with a non default value, initializer_list lets you do that with a literal list. It's the same point as with local variables described above, but withinenmer initialization you can't create named temporaries.

author: TheCalligrapher
author_rank: Regular Contributor
timestamp: July 21, 2025, 03:29:36 pm
content: Quote from: ejeffrey on July 21, 2025, 02:39:02 pm(Personal attacks excluded from the quote.)Well, this is a completely different point. Something as specific as "initialize `std::vector` class member with a literal list" and something as generic as "initialize a class member to avoid double initialization" are two completely different topics. Which is, incidentally, my pointFirstly, in your previous message you explicitly mentioned the matter of avoiding double initialization of class members as one key feature in this context (I quote: "...This makes it much easier to avoid default initialization followed by assignment.") But, again, avoiding double initialization is exactly what constructor initializer lists are for. They have always been present in the language and they served that purpose since C++98. And, as I stated above, this has nothing to do with `std::initializer_list<>`.Secondly, the ability to use list-initialization in constructor initializer lists is indeed new in C++11, however this has nothing to do with `std::initializer_list<>` either. This new ability is a consequence of so-calleduniform initialization . And uniform initialization has no direct relation to `std::initializer_list<>`. In case of `std::vector` member initialized with a list, `std::initializer_list<>` does indeed serve as an intermediary. In case of `std::array` member initialized with a list, `std::initializer_list<>` plays no role at all.Moreover, why class members? This ability has absolutely nothing to do with class members. Before C++11 (and its uniform initialization) you could not initialize an `std::vector` with an explicit listanywhere. You could not initialize a local variable like that. You could not initialize a static variable like that. Why are you suddenly talking about class members specifically, if your point is about initializing `std::vector` with literal list?Thirdly, initial forms of list initialization have been present in C++ since the beginning of times (in fact, it is rooted in good old C). Uniform initialization in large degree is just an extension of that ancient feature. Yet again, `std::initializer_list<>` is still beside the point here.So, again, why this matter is suddenly pulled into this discussion is still beyond me. That's what I'm trying to figure out, but apparently there's no hope...

author: gf
author_rank: Super Contributor
timestamp: July 21, 2025, 04:39:00 pm
content: Quote from: TheCalligrapher on July 21, 2025, 03:29:36 pmBut now (C++11 or later) you can, and that's where std::initializer_list comes into the play, since a proxy object of type std::initializer_list<T> is used to pass a brace-enclosed initializer-list to one of the constructors of std::vector<T> when you write e.g.Code:std::vector<int> v = { 1,2,3 };orCode:std::vector<int> v { 1,2,3 };Under the hood, this constructor overload is invoked:Code:std::vector<int>::vector( std::initializer_list<int> init, const Allocator& alloc = Allocator() );

author: TheCalligrapher
author_rank: Regular Contributor
timestamp: July 21, 2025, 05:38:21 pm
content: Quote from: gf on July 21, 2025, 04:39:00 pmThat's very nice. But my original issue was with the strange mention of class member and "avoiding assignment after initialization for class members", which is a completely separate matter.---On a related note, it is probably worth nothing that brace-enclosed list is not anexpressionin C++. It has no type. It is not `std::initializer_list<>` and it is not related to `std::initializer_list<>` in any way (contrary to a rather widespread mistaken belief).Brace-enclosed list is just a grammar-level construct that is parsed as an initializer in a number of  contexts. The only connection it has to `std::initializer_list<>` is that it can be used toinitialize`std::initializer_list<>`. But brace-enclosed list can be used to initialize many other things as well, not just `std::initializer_list<>`. And thanks to uniform initialization, this in turn can be used to pass brace-enclosed lists around. For example, one can use a classic old naked C-style array to pass a "brace-enclosed initializer-list" to a functionCode:template <typename T, std::size_t N> void foo(const T (&a)) { /* ... */ }...foo({ 1, 2, 3 });foo({ "a", "b", "c", "d" });No `std::initializer_list<>` here, as you can see. One can use `std::array`, `std::tuple`, variadic templates and a number of other language features to achieve the same effect. However, the distinctive property of `std::initializer_list<>` is that it isrun-time sized, as opposed to compile-time sized alternatives (e.g. my example above). Run-time sizing is basically the only (!) distinctive property of `std::initializer_list<>`, itsraison d'être.Thanks to `std::initializer_list<>`, I can re-implemenet the above without having `N` as a template parameterCode:template <typename T> void foo(std::initializer_list<T> a) { /* ... */ }That's about it. That is what `std::initializer_list<>` is actually for: support for convenient short-lived storage ofrun-time sizedsequences of identically typed elements.

author: gf
author_rank: Super Contributor
timestamp: July 21, 2025, 07:58:13 pm
content: Quote from: TheCalligrapher on July 21, 2025, 05:38:21 pmI tried the same with std::array instead of a plain array, but it can neither deduce template parameters T + N, nor N alone.What's wrong with foo2 and foo3?https://godbolt.org/z/4cj3nP54T

author: TheCalligrapher
author_rank: Regular Contributor
timestamp: July 21, 2025, 08:11:00 pm
content: Quote from: gf on July 21, 2025, 07:58:13 pmI think I got a bit too optimistic about other opportunities. Indeed, `std::array`, `std::tuple` etc. will not be able to deduce template parameters from brace-enclosed initializer list. Only plain C-style array can as well as `std::initializer_list<>`. So, this is another reason for `std::initializer_list<>` to exist: it uses a dedicated set of type deduction rules tailored specifically for it.Variadic templates will also refuse to accept brace-enclosed initializer list as an argument, although within the context of the original question they still might be useful, just without the `{}` around the list.

author: gf
author_rank: Super Contributor
timestamp: July 21, 2025, 08:37:03 pm
content: Quote from: TheCalligrapher on July 21, 2025, 08:11:00 pmWhile deduction did not work in conjunction with a function call, it is interesting that it does work when a std::array variable is initialized:https://godbolt.org/z/TdGMh9M6o

author: TheCalligrapher
author_rank: Regular Contributor
timestamp: July 21, 2025, 08:58:11 pm
content: Quote from: gf on July 21, 2025, 08:37:03 pmYes, since C++17 we have template argument deduction for class templates as well (CTAD). However, this process does not naturally pass through function parameters to function template arguments unfortunately, i.e. there's no such thing as implicit "CTAD followed by FTAD" deduction in C++. It appears that the only way to take advantage of CTAD in such cases is to do something likeCode:template<typename T, std::size_t N> void foo2(const std::array<T, N> &) { }int main(){foo2(std::array{ 1, 2, 3, 4 });}Note that in this version of the code the template arguments are still "deduced", since we are not supplying them explicitly anywhere. Yet, we are forced to trigger "early deduction" by mentioning `std::array` at the call site, essentially turning it into an explicit "CTAD followed by FTAD" two-step deduction.