This seems counterintuitive to me. I consider `sizeof` and `decltype` a poor version of a reflection and you can do this:

struct X {
int a;
static constexpr unsigned size = sizeof(X::a);
using type = decltype(X::a);
};

https://compiler-explorer.com/z/9f7WKn

Even when the type is not yet complete. I personally think we should support such cases.

Default member initializers are parsed in a complete-class-context", essentially meaning the parsing is delayed until the closing } of the class. This is why lookup works in those cases. The same is true of default arguments, inline member function bodies, and noexcept-specifiers.

My mental model of injection via conseval blocks is very simple. First, consteval blocks have the guarantee that they are executed where they appear or where they're instantiated if they appear in a template. After the metaprogram runs, the compiler effectively "pastes" the injected code over top of the metaprogram as if you had manually typed in. Of course, whatever rules and restrictions apply to hand-written code also apply to metaprograms.

Adding consteval blocks to the complete-class-context might make lookups work in metaprograms, but that also breaks this very simple model. Consteval blocks are no longer guaranteed to execute where they appear, but rather sometime later. That means you can't rely on declarations assumed to have been injected:

template<typename T>
struct my_class {
   consteval { -> inject_useful_decl<T>(); }
   decltype(useful_decl) var; // oops. error: no useful_decl declared.
};

I think it also makes reasoning about injection much harder because the complete-class-context defers analysis to the outermost enclosing class.

struct s {
  struct  t {
    consteval { /* inject code */ };
  }; // metaprogram is not run here
}; // metaprogram runs here

This potentially makes the implementation a bit harder too, since we'd have to re-establish the full context where the consteval block was parsed before executing it.

Andrew