Is there a way to make something like the following work? Since all sizes are known at compile-time it seems to me that it should, but no luck so far...
I believe this is not possible. The problem is that there's currently no way to express allocating the variable-length [u8] in a const-generic context. Just considering getting the length itself right and not the contents, if it were possible it would have to start out something like:
Therefore, it is harder to understand the behavior of a Rust program than if panicking did not exist; for example, if you see fn a() { b(); c(); } and you know a was called, then you still cannot be sure c will be called.
If we allow the type of a value (here, an array [u8; 6 + T::X.len()]) to depend on a computed expression (here, 6 + T::X.len()), then type solving involves expression evaluation, which can panic (in many cases, due to numeric overflow, but not exclusively that).
Therefore, if we allow this, it may be the case that some type claims to implement a trait, but actually instantiating the trait implementation panics.
Thus we have lost the property of Rust that, if a trait bound is met, then you can usually expect the code to work — there won’t be undeclared, unmet requirements.
Now, in fact, we already don't have this property, because evaluating the value of a constant can panic. But, allowing this would make it worse, because it would mean the types of the program aren't even defined, so you couldn't even analyze the program, let alone compile it. Probably we'll go ahead and do that anyway — but the compiler isn’t yet equipped to do that, in part because the maintainers were reluctant to allow these kind of errors (“post-monomorphization errors”).
In your particular case, because you are trying to construct an &[u8] and not an &[u8; N], this is not actually blocking (other than for constructing the array itself). But there’s simply no operation in the language which means “allocate a dynamic amount of memory at compile time”, so there's no way left to get the memory for your [u8]. We can't have compile-time Box::new() or Vec::new() because those things are defined to use a specific heap allocator that allows the memory to be freed, and the heap doesn't exist yet so nothing can be put in it.
There’s more to everything here, of course; I'm sure you can imagine a few “what if we just…” solutions to these problems. Some of those have their own problems, but a lot of this is simply that more design and implementation work is needed.
That's pretty nice explanation of the low-level details of the problem but you neglected to tell what's the core problem, itself.
After all C++ can do all these things and more and it also have panics, in form of exceptions.
The core issue is arbitrary, yet important, decision of Rust to go with generics, not templates.
What's the difference? Types. Generics are typed, templates are not. Generics meaning is determined at the definition point, templates meaning is determined at the instantiation point.
That means that if you ask someone “does foo<X>” have a meaning in Rust you only need to look on declaration of X and not on definition. It doesn't matter how foo is implemented, you only need to know what traits are defined for X and what traits are needed by foo.
In case of C++ situation is very different: not only do you need to know which concepts are implemented by foo, you need to also try to instantiate foo and only then would you know the answer.
Surprisingly enough, in Rust, you also may need to perform to do arbitrary calculations before you'll know if foo<X> is actually usable (and not just “have a meaning”), @alice example shows why… this cheapens Rust's guarantees and, practically, means that sacrifice doesn't buy us much… but that's the core [pretty artificial] problem that limits Rust.
All these troubles with panics and other stuff comes from that decision.
I've used this trick in a generic context, just that the buf size was large enough for all use cases and I had const asserts that result is not bigger.
The only downside that for some reason Rust will include the whole buffer into the executable, not just the part you end up referencing.
So if you expect maximum size of the string to be 512, you set the constant there and in the binary every string constructed that was is 512 bytes. Usually, that cost would be negligible.