Throwing exceptions might not be acceptable in your use-case, with many codebases disabling it for concerns of performance or executable bloat. And factory functions cannot really help the initialization problem anyway, since your field is still in an uninitialized state before calling the factory function. And also, returning std::optional or std::expected can sometimes lead to slower code (copy constructor being called) because of NRVO not working when the compiler “feels like it”. So an init() method/function that returns an error code is the best you can get.
I think the best way to really solve the invalid object problem in C++, is to always put sensible default values inside your struct/class definitions, so your code will at least fail without UB (in addition to using asserts liberally to check for index overflow problems, especially in your own containers even in. Release mode.) Zero-initialization is good for most cases, but sometimes you might need to default-initialize your indices to -1.
weak_ptr is really slow, because of the lock instruction generated for each lock(), as well as the added indirection cost. Once you use them all over the place to fix reference cycles, the overhead of using these becomes hard to ignore. Generational indices/references remove all the ref-counting and gives programmers the semantics of weak_ptr without the performance cost (see https://verdagon.dev/blog/generational-references, the details are mostly language-agnostic).