指向不可变类型的共享指针具有值语义

Shared pointer to an immutable type has value semantics

本文关键字:语义 指针 共享 不可变 类型      更新时间:2023-10-16

Sean Parent在2013年Go Native Original上发表了题为"继承是邪恶的基础类"的演讲。在20分50秒后,他声明指向不可变(const)类型(std::shared_pointer<const T>)的共享指针具有值语义。这到底是什么意思?为什么它与指向可变(非常量)类型(std::shared_pointer<T>)的共享指针有任何不同?

不幸的是,就像2013年Go Native 的所有演讲一样,它受到非常紧迫的时间安排的限制。不过,对我们来说幸运的是,Sean Parent去年在C++Now上做了一个更彻底的演讲,名为价值语义和基于概念的多态性。它涵盖了相同的材料,可能会回答您的问题。无论如何,我都会尝试解释....

介绍

类型可以具有两种类型的语义:

  • 值语义。
  • 引用语义。(有时称为指针语义。

可以继续许多页面,了解两者有何不同以及何时一个比另一个更可取。简单地说,使用值类型的代码可以更容易地推理。

也就是说,值类型的实例在任何时候都不会发生任何不可预测的事情 - 引用类型无法保证这一点,因为引用的值在代码中保存对它的引用的其他部分之间共享。

换句话说:引用类型的可预测性较低,因为它们可以通过一段遥远的代码进行更改。例如,您调用的函数可能会更改从您下方引用的值。或者,更糟糕的是,如果涉及线程,则引用类型可能随时由碰巧对引用的值进行操作的另一个线程更改。出于这个原因,Sean Parent 声明,当涉及到能够推理使用全局变量的代码时,shared_ptr与全局变量一样好

综上所述,我们应该准备回答手头的问题。


问与答

对于值类型T,为什么shared_ptr<const T>即使它是指针类型,它的行为也像值类型?

由于我们无法对指向的const T进行更改,因此有关指针/引用类型更难预测的所有内容都不再适用。我们不再需要担心T被意外更改,因为它是一个 const 值类型。

如果我们确实想 改变T ,我们必须复制它,让其他持有shared_ptr<const T>的人不受我们行为的影响。此外,副本甚至可以使用称为写入时复制的机制隐藏在值类型中,这似乎是 Sean Parent 最终所做的。


我想我已经像肖恩·帕伦特那样回答了这个问题(并在链接的C++现在演示文稿中做到了),但让我们进一步

补充......

一个大附录:

(感谢@BretKuhns提出这个问题并在评论中提供示例。

这整个概念有一个令人讨厌的错误。说shared_ptr<const T>的行为像值类型不一定正确,除非我们知道T实例的所有活指针/引用都是const。这是因为const修饰符是单行道——持有shared_ptr<const T>可能会阻止我们修改T的实例,但不会阻止其他人通过指向非const的指针/引用来修改T

知道了这一点,我会厌倦做出shared_ptr<const T>与值类型一样好的宽泛声明,除非我知道所有指向它的活生生的指针都是const。但是,了解这样的事情需要对shared_ptr<const T>的所有用法的代码进行全局了解 - 这对于值类型来说不是问题。出于这个原因,这样说可能更有意义:shared_ptr<const T>可用于支持值语义


顺便提一下,我实际上是在2013年本土化 - 也许你可以在左前方看到我的后脑勺。

我举了3个例子。在所有三种情况下,我都创建了一个带有内容"original value"的变量a。然后我通过说auto b = a;来创建另一个变量b,在此语句之后,我a分配内容"new value"

如果ab有价值语义,我希望b的内容是"original content"。事实上,这正是发生在stringshared_ptr<const string>.auto b = a;的概念含义与这些类型相同。与其说是shared_ptr<string>b将有内容"new value"

代码(在线演示):

#include <iostream>
#include <memory>
#include <string>
using namespace std;
void string_example() {
    auto a = string("original value");
    auto b = a; // true copy by copying the value
    a = string("new value");
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    cout << boolalpha << "&a == &b ? " << (&a==&b) << endl;
}
void shared_ptr_example() {
    auto a = make_shared<string>("original value");
    auto b = a; // not a copy, just and alias
    *a = string("new value"); // and this gonna hurt b
    cout << "a = " << *a << endl;
    cout << "b = " << *b << endl;
    cout << boolalpha << "&a == &b ? " << (&a==&b) << endl;
}
void shared_ptr_to_const_example() {
    auto a = make_shared<const string>("original value");
    auto b = a;
    //*a = string("new value"); // <-- now won't compile
    a = make_shared<const string>("new value");
    cout << "a = " << *a << endl;
    cout << "b = " << *b << endl;
    cout << boolalpha << "&a == &b ? " << (&a==&b) << endl;
}
int main() {
    cout << "--------------" << endl;
    cout << "string example" << endl;
    string_example();
    cout << "------------------" << endl;
    cout << "shared_ptr example" << endl;
    shared_ptr_example();
    cout << "---------------------------" << endl;
    cout << "shared_ptr to const example" << endl;
    shared_ptr_to_const_example();
}

输出:

--------------
string example
a = new value
b = original value
&a == &b ? false
------------------
shared_ptr example
a = new value
b = new value
&a == &b ? false
---------------------------
shared_ptr to const example
a = new value
b = original value
&a == &b ? false

话虽如此,我希望他能多一点时间:在那次演讲之后,我仍然想知道一些事情。我很确信这只是时间不够,他似乎是一位出色的主持人。

他的意思是,它们可以用来模拟值语义

价值语义的主要定义特征是具有相同内容的两个对象是相同的。 整数是值类型:5 与任何其他 5 相同。 将其与参考机制进行比较,其中对象具有标识。 包含 [1, 2] 的列表a与包含 [1, 2] 的列表b不同,因为将 3 追加到 a 与将 3 追加到 b 的效果不同。 a的身份不同于b的身份。

这往往是直观的...用语言表达时听起来很奇怪。 没有人能在C++ 3 天内没有对值类型与引用类型有一些直观的了解。

如果你有一个可变值类型,并且你想要复制它,你必须实际复制对象的内容。 这很昂贵。

肖恩所指的技巧是,如果一个对象是不可变的,那么你不必复制整个对象,你可以只引用旧的对象。 这要快得多。

他似乎假设shared_ptr<const T>的存在意味着对象的所有句柄也是shared_ptr<const T>的(也就是说,只读)。

当然,这并不比原始const T*的存在构成对象const的证据更真实。

演示:http://ideone.com/UuHsEj

可能你把"不变性"误认为是意义const T——在问题中你说它们是相同的,但它们不是。