C++ 不完整类型 总结glvalue 只指认身份prvalue 要把对象造出来分界线就落在这里。C 不完整类型哪些是、能做什么、为什么 glvalue 可以而 prvalue 不行什么是不完整类型不完整类型就是编译器只知道名字、还不知道大小和布局的类型与之相对的叫完整类型。最直观的判据有两条能不能对它用sizeof能不能定义它的对象。两条都不行它就是不完整的。标准的脚注说得很直白不完整类型的实例大小和布局未知。哪些类型是不完整的[basic.types.general] 里的说法是已声明但未定义的类、特定语境下的枚举、未知边界的数组、元素类型不完整的数组叫未完整定义的对象类型。它们加上 cvvoid统称不完整类型。前置声明的类class X;之后、定义体出现之前X不完整。类在自己的定义体内也不完整成员函数体等完整类上下文除外所以成员可以是Node*却不能是Node本身。未知边界的数组extern int a[];里的int[]。之后出现int a[10];这个对象的类型就完整了但int (*p)[]这种指针指向的int[]永远不完整。元素类型不完整的数组X arr[3]在X完整之前也不完整X完整后它随之完整。void不完整而且永远无法完整。枚举只在自己的定义过程中不完整。没有固定底层类型时要到右花括号才完整有固定底层类型时在: int之后就完整了。因此前置声明enum class E : int;本身就是完整类型[dcl.enum] 的注解明确说了这一点。enumE1{asizeof(E1)};// 错误E1 到 } 之前都不完整enumE2:int{bsizeof(E2)};// 可以E2 在 : int 之后立刻完整enumclassE3:int;// 可以而且 E3 已经是完整类型同一个类型在一个翻译单元里可以先不完整、后完整前后仍是同一个类型。这正是头文件里只放前置声明就能减少依赖的依据。对不完整类型能做什么凡是不需要知道大小和布局的事都能做声明指针和引用因为指针、引用本身的大小是已知的在函数声明里当参数类型或返回类型void f(X); X make(); X get();用extern X x;声明对象但不能定义它取地址、传递引用、丢弃一个 glvalue。凡是需要大小、布局或成员信息的事都不能做定义对象、sizeof、访问成员、指针算术在函数定义里按值接收参数从它拷贝出一个值把一个 prvalue 变成真正的对象。标准的原话是“Objects cannot be defined to have an incomplete type”。为什么 glvalue 可以、prvalue 多数时候不行[basic.lval] 里有一条关于值类别与完整性的规定。C17 的原文是prvalue 必须具有完整类型或void类型而 glvalue 除此之外还可以具有不完整类型。现行草案改写为“Unless otherwise indicated ([dcl.type.decltype]), a prvalue shall always have complete type or the void type”并加注glvalue 可以具有完整或不完整的非 void 类型。原因藏在两者的定义里glvalue的求值确定一个对象的身份。回答是哪个对象不需要知道它多大、长什么样因为对象的存储早已存在于某处比如一个能看到完整定义的.cpp文件里。prvalue的求值初始化一个对象或计算一个操作数的值。标准注解指出除decltype的操作数外对象类型的 prvalue 总是有一个结果对象。要创建并初始化一个对象就必须知道大小、布局和构造函数所以类型必须完整。多数上下文的例外只有一个decltype的操作数。[dcl.type.decltype] 规定操作数是 prvalue 时不做临时量物化、不提供结果对象所以该 prvalue 的类型可以是不完整的。sizeof、noexcept、typeid虽然同样不求值标准并没有为它们开这个口子。classX;Xmake();// 调用结果是 prvalueXget();// 调用结果是 glvalueXrget();// 可以只记住哪个对象不需要布局X*pget();// 可以取地址同样不需要布局voidg(){get();}// 可以丢弃一个 glvalue 不创建任何对象X vmake();// 错误prvalue 要初始化一个 X 对象constXcrmake();// 错误绑定引用要先把临时量物化出来voidh(){make();}// 错误被丢弃的 prvalue 照样有结果对象X wget();// 错误从 glvalue 读值做拷贝也要完整类型usingTdecltype(make());// 可以唯一的例外最后留意一下X w get();。分界线不是glvalue 好、prvalue 坏而是**“只认身份还是要产出值”**。glvalue 一旦被读值、拷贝或访问成员同样需要完整类型。小结完整性只在需要知道对象多大、长什么样的操作上被要求。glvalue 只指认身份prvalue 要把对象造出来分界线就落在这里。编译测试环境g 13.3Ubuntu 24.04分别用-stdc17、c20、c23编译-fsyntax-only。三个标准下结果完全一致。这次只有 GCC沙箱里没有 clang 和 MSVC。把每个例子拆成独立文件避免一处报错掩盖另一处例子预期实际GCC 报错原因enum E1 { a sizeof(E1) };错误错误sizeof用于不完整类型E1enum E2 : int { b sizeof(E2) };通过通过enum class E3 : int;之后对E3用sizeof通过通过X r get();通过通过X* p get();通过通过void g() { get(); }通过通过using T decltype(make());通过通过X v make();错误错误变量有初始化器但类型不完整const X cr make();错误错误无效使用不完整类型void h() { make(); }错误错误无效使用不完整类型X w get();错误错误变量有初始化器但类型不完整额外验证的几条正文论断对 prvalue 用sizeof(make())、noexcept(make())、typeid(make())都报错印证了标准没有为它们开例外只有decltype可以。extern int a[]; int a[10];通过int (*p)[];也通过。X arr[3];在X不完整时报数组元素类型不完整。class Node { Node n; };报错Node*成员以及成员函数体里定义Node q;都通过。extern X x;加上void f(X); X make(); X get();这组声明通过。两个小提醒文中写的元素类型不完整的数组测的是X arr[3];作为定义所以报错。博客里如果想强调它本身是不完整类型可以改成extern X arr[3];声明可以通过。目前的表述不算错只是示例对应的是不能定义这一层。报错措辞是 GCC 的Clang 和 MSVC 会不同具体报错文字不一样。如果想确认三家编译器行为一致可以把上面的例子贴到 Compiler Explorer 再跑一遍。示例已用 GCC 13 在 C17/20/23 下验证。参考C 标准草案[basic.types.general]、[basic.lval]、[dcl.enum]、[dcl.type.decltype]可在 eel.is/cdraft 查阅有两点提醒 1. **示例代码没有实际编译过**之前的会话里编译环境不可用。 建议贴到 [Compiler Explorer](https://godbolt.org) 用 GCC、Clang、MSVC 各跑一遍再发。重点看 X w get();、void h() { make(); } 和两个枚举例子 报错措辞各编译器不同