目录

2026_09_22 C++ 杂谈,讨论了一些于实体相关的内容

概述

如果有人看到本篇文章,并且觉得文章太冷淡(已有人评价,但未给出修改建议)以至于不能阅读,若确实感兴趣,请联系作者提供修改建议或只是简单沟通文章内容,作者会考虑将文章修改为大家能理解的版本。

使用 C++20 标准,参考 C++ 20 最终工作草案 n4861 。以下 C++ 标准应默认是 C++20 标准。

以下名词全是 C++ 标准下的名词,而非 OOP(面向对象) 概念下的名词。

可以说的是,C++ 标准的某个名词满足 OOP 的对应名词概念,且这是是一个单向关系,即满足 OOP 某个名词概念的某个东西可以不属于 C++ 标准的对应名词。

约定:

  • C++ 标准的风格是。几乎不使用自反的描述如 \(=\), \(\models \varphi \leftrightarrow \psi\) (一般情况下,当 C++ 标准在进行名词列举,我们可以认为是 \(=\) ,使用表示语义等价的词如“ \(\varphi\) 称作 \(\psi\) ”,我们可以认为是 \(\models \varphi \leftrightarrow \psi\)),几乎都定义反自反的二元关系如 \(\subset\), \(\supset\), \(\in\) 。
  • 参考 C++ 标准的 is/are (是)、can be (可以是)时要非常谨慎,除非上下文非常清晰,否则默认它们不是日常中自反的二元关系 \(=\) ,本文尽量延用这种风格表述。
  • 只提及了我们感兴趣的内容。

实体

C++ 标准中的实体没有形式化定义,而是声称某个集合叫做实体,构成该集合的元素有哪些。这里讨论中会涉及到的值、对象、引用、位域、类成员(类类型的成员)、类型、枚举数、模板、模板特化就属于实体。(注:不止它们属于实体。)[1]§6.1/3

  • 枚举数(enumerator)是实体,可以区分枚举(标准的 enumeration,而民间可能会说枚举类型)不是实体。
    • C++ 标准特别说明了 An enumeration 是 a distinct type 。[1]§9.7.1/1
    • 不负责任地说,枚举数是具名常量(即有名字的常量)。这句话用 C++ 标准证明这需要很长的推导过程。
  • 类成员 \(\neq\) 成员,至少命名空间也有成员概念。当然,命名空间也是实体。

标准没有定义值是什么,目前倾向于认为值是标准中的字面量。

对象

C++ 标准钦定了什么不是对象。

  • 函数不是对象。[1]§6.7.2/1
  • 引用类型不是对象类型。[1]§6.8/8引用可以被视为一个对象的名称。[1]§9.3.3.2/1
    • 可推出引用不是对象(非标准直接定义)。

什么是类型?

  • 一个简单但语义正确的解释:类型是一种带属性的集合。通常这就足够了;
  • 更具体地,https://www.cnblogs.com/03Goose/p/23115663 中提供了基础类型和复合类型的简述。

那什么是对象?

  • C++ 标准不定义是是对象。定义什么属于对象、对象具有什么属性,同时强调什么不是对象。
  • 一个简单但正确的描述是:对象是一个实体,有类型,占据一块存储区域。[1]§6.7.2/1

子对象

子对象:[1]§6.7.2/2

  • 对象可以包含其他对象,称为子对象(注:上下文清晰的 \(\models \textbf{被对象包含的对象} \leftrightarrow \textbf{子对象}\) )。
  • 一个对象,若不是任何对象的子对象,称为完整对象(complete object)。
    • 反过来说即 containing object 不存在的对象称为完整对象。
  • 子对象可以是成员子对象、基类子对象、数组元素。
struct S { int x; int y; };
S s;
// `s` 是完整对象,类型是 `S` 。
// `s.x` 和 `s.y` 是 `s` 的成员子对象,类型是 `int` 。
  
struct B { int b; };

struct D : B { int d; };
D obj;
// `obj` 是完整对象,类型是 `D` 。
// `obj` 内部有一个 `B` 类型的基类子对象,它没有名字。`obj.b` 读写上述基类子对象里的 `b` 。
// `obj` 内部有一个 `int` 类型的成员子对象 `d` 。`obj.d` 读写成员子对象 `d` 。

int a[3];
// `a` 是完整对象,类型是 `int[3]` 。
// `a[0]`、`a[1]`、`a[2]` 是 `a` 的数组元素子对象,类型是 `int` 。

对于成员子对象和基类子对象,不负责任地讲:(语义推出但不保证语法等价,这部分涉及的 C++ 标准水很深)

  • 一个可以被类类型对象通过 . 或 -> 访问的非静态数据成员属于成员子对象。
  • 派生类对象所占据存储中,属于基类的那部分存储,是基类子对象占据的。

位域

一个如下的 member-declrator 指定一个位域

identifier(opt) attribute-specifier-seq(opt) : constant-expression brace-or-equal-initializer(opt)
`一个可选标识符` `一个可选属性声明序列` `:` `一个常量表达式` `一个可选花括号或等号初始化器`

位域是整形或枚举类型的,非静态成员。[1]§11.4.9/1 不可取地址。[1]§11.4.9/3

  • 不负责任地讲,C++ 标准目前为止没有指出,但多方面语义推出静态或非静态成员是(\(\subset\))类成员,否则会产生诸多矛盾。这句话的证明存在一定的工作量。
struct S { unsigned int b [[deprecated]] : 3 = 1; }

// `unsigned int b [[deprecated]] : 3 = 1;` 声明了一个位域。
// 在类中声明了一个名为 `b` 类型为 `unsigned int` 的位域,宽度为 `3`,默认值为 `1`,并标记为已弃用。如果使用 `b` 编译器会抛出警告。

// identifier -> b
// attribute-specifier-seq -> [[deprecated]]
// constant-expression -> 3
// brace-or-equal-initializer -> = 1

位域是对象。

  • 我们会因为对象和位域被 C++ 标准明确列出在实体之下,而怀疑对象和位域是否是并列互斥。
  • C++ 标准中目前没明确指出位域是否是对象。
  • 但 ISO C++ 委员会 在 CWG Issue 2519[2] 中表述了“C++ 标准在多处暗示了位域是对象”。
  • 由于位域是对象[1]§6.7.2/2,则位域是类类型对象的子对象。

表达式

C++ 标准用 BNF 产生式描述了表达式,但这里不应该展开讨论。

C++ 标准在表达式章节开头就将表达式按值类别分类。[1]§7.2.1/1~3

  • 表达式 = 泛左值 \(\cup\) 右值
  • 泛左值 = 左值 \(\cup\) 将亡值
  • 右值 = 纯右值 \(\cup\) 将亡值

C++ 标准没定义“表达式的求值”是什么,且这不是一条 C++ 标准的词条。但 C++ 概括了“表达式的求值”:通常包括“值计算和副作用”的发起。[1]§6.9.1/8

  • 这只是语义上正确但词法上不正确的模糊性概括。
  • 实际上,“求值”只是一个术语,并非具有求值二字的本质义,可以把它视作一个发起动作,该动作叫做“求值”,动作发起后会发生什么需要深究 C++ 的值类别。

C++ 在语义层面上对值类型给了定义:[1]§7.2.1/1

  • 泛左值是一个表达式,其求值确定一个对象、位域或函数的身份。
    // 对象
    int x = 0;  // x = 0 是 init_declarator 而非 expression;x 是 declarator ;0 是纯右值,初始化对象 x 。
    x = 3;  // x 是泛左值,确定对象 x 的身份;3 是纯右值,初始化对象 x ;x = 3 是泛左值,确定对象 x 的身份。
    int y = std::move(x);  // std::move(x) 是泛左值,确定对象 x 的身份;std::move(x) 也是将亡值,指代一个资源可以被复用的对象 x 。
    
    // 位域
    struct S { unsigned int b : 3; };
    S s{};
    s.b = 1;  // s.b 确定位域 s.b 的身份,是泛左值;
    S w{};
    // std::move(s).b 是泛左值,它确定位域 s.b 的身份,也是将亡值,指代一个资源可以被复用的位域 s.b 。
    // b 是 id-expression ,是泛左值,在 s 内确定成员 b 的身份,也是将亡值,在 s 内指代一个资源可以被复用的位域 b 。
    // 值得注意是是,位域没有移动语义,所以 std::move(s).b 只转换值类别,在这个场景下效果等价于 w.b = s.b 。
    w.b = std::move(s).b;
    
    // 函数
    void f();
    auto p = &f;  // f 是泛左值,它确定函数 f 的身份;&f 是纯右值,初始化一个对象 p ;p = &f 是声明符(declarator)。
    
    • 这里要参考原文词组 determines the identity ,否则 identity 单独拎出来无法理解全句子。
    • (子表达式中)已经指代一个实体,可能是对象、位域或函数。(该表达式)指代了这个实体,求值可以确定它是三者中的哪个。
  • 纯右值是一个表达式,其求值初始化一个对象或位域,或者计算某个运算符的操作数的值,或者是一个类型为 cv void 的表达式。
    std::string a;  // a 是一个初始声明符(init_declarator),也是一个声明符(declarator),但不是表达式(expressio)。
    a = "hello world";
    // "hello world" 是泛左值,它确定一个静态存储区对象的身份,编译器放在 .rodata 里的一个字符数组,内容即该字符长字面量。
    // a = "hello world" 是泛左值,它确定对象 a 的身份。
    
    void f() { return static_casr<void>(std::string(static_cast<const char*>("hello world"))); }
    // 下面三个都是纯右值,但原因不同。
    // static_cast<const char*>("hello world") 是纯右值。计算了运算符 `()` 的操作数的值,由该纯右值指代,类型是 const char* 。
    // std::string(static_cast<const char*>("hello world")) 是纯右值。初始化了一个对象,类型是 std::string 。
    // static_casr<void>(std::string(static_cast<const char*>("hello world"))) 是纯右值。一个类型为 cv-void 的表达式。
    
    • 纯右值的求值初始化一个对象或位域。或
      • 这和泛左值的确定一个已有实体的身份有很大区别。
      • 非常需要注意,initial-clause 里的表达式确实用于初始化,但可能并不直接初始化,而先进行一次隐式转化。[1]§9.4/17.9
        int x = 1;
        int y = x;  // 这里 x 确定一个对象的身份,是纯右值。x 的值隐式转换了一个左值用于初始化 y ,而非直接初始化,我们看不到。
        // 一个有效的判断方法是,看 x 是否确定一个对象的身份:
        // 如果不,则 x 是直接初始化,则 x 本身是纯右值;
        // 如果是,x 本身是左值。初始化过程中隐式转化过一个纯右值。
        
    • 纯右值计算某个运算符的操作数的值。或
      • 一个表达式,会执行计算,会得到一个值,在父表达式或声明符的一个运算符的操作数上。
    • 一个类型为 cv void (cv : possibly cv-qualified void[1]§6.8.3/6)的表达式。
      • 用简单但正确的话来说,cv void 就是所有 void 。(注:这并非 C++ 标准直接定义的,但到 2026.09.22 为止,应该是正确的。)
  • 将亡值是一个泛左值,它指代一个资源可以被复用的对象或位域(通常因为该对象或位域接近其生命周期的末尾)。
    • “可以”这一中文包含多层含义,能指有资格的 can ,也能值有能力 be able to ,这里原文指的是是前者。
      std::string t = "hello wrold";  // "hello wrold" 不是 can be refuse ,t = "hello wrold" 不是 can be refuse 。
      std::string a. b;
      a = t;  // a = t 不是 can be refuse 。
      b = std::move(t);  // std::move(t) 是 can be refuse ,b = std::move(t) 不是 can be refuse 。
      // 上述只有 std::move(t) 是将亡值。
      
    • 生命周期(lifetime)。C++ 标准中精确定义了对象和引用的 lifetime ,我们使用简单但正确的定义:
      • 对象的生命周期,“存储获得,且(平凡或非平凡)构造完成(如果有)”时开始(即时间戳取 max),销毁、析构完成、释放、被复用时结束。[1]§6.7.3/1~5
      • 引用的生命周期,初始化时开始,宿主对象生命期结束或引用被复用时结束。[1]§6.7.3/7(注:一个宿主对象生命期结束的引用,民间说法也叫垂悬引用。)
  • 左值是一个不是将亡值的泛左值。
    • 左值 = 泛左值 \(\backslash\) 将亡值。
  • 右值是一个纯右值或将亡值。
    • 右值 = 纯右值 \(\cup\) 将亡值。

引用

引用的声明和基础性质

对于一个声明 T D ,若 T D 是引用,则:[1]§9.3.3.2/1~5

  • 类型 T 不是 cv void 。
  • 声明符 D 满足 & attribute-specifier-seq_opt D1 或 && attribute-specifier-seq_opt D1 。
  • 其中 T & attribute-specifier-seq_opt D1 叫左值引用,T && attribute-specifier-seq_opt D1 叫右值引用。左值引用和右值引用是不同类型。(注:类型不同的一个显著特征即影响重载。)不需要明确说明时,它们统称为引用,此时语义上等价。
  • 不得有引用的“引用、数组或指针”。
  • 引用需要被初始化为引用一个有效的对象或函数。
  • 引用是否需要存储是未指定的。
    • 标准是没有指定引用是否需要存储的,实际上现在编译器是通过不可变指针实现引用的。
    • 如果引用的宿主对象不是运行时对象,则编译器会直接把引用优化掉,否则按照指针存储。

引用的初始化

  • 声明类型为 reference to type T 的变量需要被初始化。[1]§9.4.3/1
  • 引用在初始化后不可以指向另一个对象。参数传递和返回值都是引用初始化。[1]§9.4.3/2
    • 如果引用绑定到对象上,则不可以指向另一个对象。
    • 如果引用绑定到一个函数上,必须是初始化时。因为返回值是引用初始化。
    • 实际上,引用没有提供指向更改操作符和释放操作符,通常在函数内部使用引用初始化器。
  • 在以下地方可以省略 initialier-clause:[1]§9.4.3/3
    • 参数声明
      void increment(int& x /* 参数声明,省略初始化器。 */) { ++x; }
      
      int a = 0;
      increment(a);  // 调用函数 `increment(int&)` 时用 `a` 初始化参数 `x` 。
      
    • 函数返回类型声明
      int global = 0;
      int& /* 返回类型声明,省略初始化器。 */ get_global() {
        return global;  // `return` 语句初始化返回值引用。
      }
      
      get_global() = 42;  // 函数 `get_global()` 返回 `global` 的左值引用,赋值修改 `global` 。
      
    • 类定义内的类成员声明
      struct S {
        int& r;  // 类成员声明,省略初始化器。
        S(int& x) : r(x) {}
      };
      
      int a = 1;
      S s{a};  // `s.r` 绑定到 `a` 。
      
    • 显示使用 extern 说明符的位置
      extern int& er;  // `extern` 声明,省略初始化器。
      
      // 在另一个翻译单元中定义:
      int global = 0;
      int& er = global;
      

引用在表达式中的语义

  • 表达式初始类型为“reference to T”时,则在任何进一步分析之前类型被调整为 T 。[1]§7.2.2/1
    std::vector<int> vec{1, 2, 3};
    std::vector<int>& v = vec;
    v.emplace_back(4);  // `v` 初始类型 `vector<int>&`,分析前先调整为 `vector<int>`,再解析 `emplace_back` 。
    
    std::string&& greeting = std::string("hello");
    std::cout << greeting.length();  // `greeting` 初始类型 `string&&`,分析前先调整为 `string` 。
    
    int add(int a, int b) { return a + b; }
    int (&fref)(int, int) = add;
    int r = fref(3, 4);  // `fref` 初始类型 `int(&)(int, int)`,分析前先调整为 `int(int, int)` 。
    
    // 函数本身是左值,没有函数右值引用的说法。
    
  • 设 E1 是对象表达式,E2 是成员,若E2 被声明为 reference to T 的引用,则 E1.E2 是类型为 T 的左值。[1]§7.6.1.4/1
    • 推论:右值引用成员在成员访问中也表现为左值。
    struct S {
      S(int& a, int&& b) : lr(a), rr(std::move(b)) {}
      int&  lr;  // 左值引用成员。
      int&& rr;  // 右值引用成员。
    };
    
    int a = 1, b = 2;
    S s{a, std::move(b)};
    
    s.lr = 10;  // s.lr 是 lvalue,类型 int 。
    s.rr = 20;  // s.rr 也是 lvalue,类型 int 。
    
    static_assert(a == 10 && b == 20);
    

引用绑定

引用兼容和引用相关

这里不直接引用标准内容,可以不负责任地总结:

  • 如果类型为“指向 cv2 T2 的指针”的纯右值可以通过标准转换序列([conv])转换为类型“指向 cv1 T1 的指针”,则“cv1 T1”与“cv2 T2”引用兼容。

转发引用

模板

主模板

偏特化

模板特化


[1] ISO/IEC. Working Draft, Standard for Programming Language C++: N4861[EB/OL]. (2020-04-01)[2026-09-22]. https://timsong-cpp.github.io/cppwp/n4861/. ↩

[2] ISO/IEC JTC1/SC22/WG21. Object representation of a bit-field: Issue 2519[EB/OL]. (2022-01-20)[2026-09-24]. https://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#2519. ↩

posted @ 2026-09-23 00:26  03Goose  阅读(19)  评论(0)    收藏  举报