【问题标题】:Shared pointer to an immutable type has value semantics指向不可变类型的共享指针具有值语义
【发布时间】:2013-09-10 02:00:00
【问题描述】:

Sean Parent 在 Going Native 2013 上发表了题为 Inheritance Is The Base Class of Evil 的演讲。在 20 分 50 秒后,他声明指向不可变 (const) 类型 (std::shared_pointer<const T>) 的共享指针具有值语义。这到底是什么意思?为什么它与指向可变(非常量)类型 (std::shared_pointer<T>) 的共享指针有什么不同?

【问题讨论】:

  • 猜测:如果我们假设没有指向非 const T 的原始指针,那么 const “保证”对象的可观察状态不会被修改。因此,拥有多个此类指针与拥有多个(不可变)对象(副本)一样好。
  • 这也让我很困惑。

标签: c++ c++11


【解决方案1】:

不幸的是,就像Going Native 2013 的所有会谈一样,它受到非常紧迫的时间安排的限制。不过,对我们来说幸运的是,Sean Parent 去年在 C++Now 上进行了更彻底的演讲,名为Value Semantics and Concepts-based Polymorphism。它涵盖了相同的材料,并且可能会回答您的问题。无论如何,我会尽力解释......

简介

一个类型可以有两种类型的语义:

  • 值语义。
  • 参考语义。 (有时称为指针语义。)

关于两者有何不同以及何时一个比另一个更可取,可以继续写很多页。让我们简单地说,使用值类型的代码更容易推理。

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

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

说了这么多,我们应该准备好回答手头的问题了。


问答

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

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

如果我们确实想对 T 进行更改,我们将不得不制作一份副本,让持有 shared_ptr<const T> 的其他人不受我们的行为影响。此外,甚至可以使用称为 Copy-on-write 的机制将副本隐藏在值类型中,这似乎是 Sean Parent 最终所做的。


我想我已经像 Sean Parent 一样回答了这个问题(并且在链接的 C++Now 演示文稿中做了),但是让我们再进一步补充一点.....

一个重要的附录:

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

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

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


顺便说一句,我实际上参加了 2013 年的 Going Native ——也许你可以在左前方看到我的后脑勺。

【讨论】:

  • 请注意,这仅从返回 shared_ptr<const Foo> 的库的角度来看是正确的。收到shared_ptr<const Foo> 的客户端不能保证Foo 的状态不会在外部发生变化。有关示例用例,请参见此代码 sn-p:gist.github.com/bkuhns/6563448,即使我的 main() 持有 shared_ptr<const Foo>Foonum() 也会更改状态。这在多线程应用程序中可能更加微妙,在该应用程序中,状态可能会在没有本地调用 doSomething() 的情况下发生变化,如我的示例所示。
  • @BretKuhns 同意,这是一个很好的观点。仅仅因为您持有指向 const 的指针/引用并不意味着其他所有人都如此。然而,讨论的重点是通过编写向客户端公开具体值类型的库代码来隐藏这些细节。 shared_ptr<const T> 的使用则成为库的实现细节。
  • @KazDragon 一个公平的观点,虽然这只意味着不应该有任何竞争条件。请注意,我的要点没有线程化,但仍然演示了问题。从foo 获取状态的两次调用可以是线程安全的,但是如果另一个线程潜入并锁定foo,则状态仍然可以在第一次和第二次调用之间发生变化,然后您才能再次调用它。
  • @MarkRansom 我们跑题了,但我一点也不觉得奇怪。类型不同,但它们引用同一个对象,因此它们的引用计数必须共享是很自然的。
  • 我意识到这是事后的事,但我想观察:在附录中,你说“整个概念”有问题。这不太对。不可变对象的 shared_ptr 具有值语义是非常正确的。但是 const T 与不可变对象不同,这正是由于 cmets 中讨论的原因。 const T 是一种类型,指针/引用可以使用它来引用 C++ 类型系统中的非 const T。如果你创建一个没有非常量方法的类型 T,那是一个不可变类型,它的 shared_ptr 具有值语义。
【解决方案2】:

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

如果ab 具有值语义,我希望b 的内容是"original content"。事实上,stringshared_ptr<const string> 正是发生这种情况。 auto b = a; 的概念含义与这些类型相同。没那么多shared_ptr<string>b会有"new value"的内容。

代码(online demo):

#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

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

【讨论】:

    【解决方案3】:

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

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

    这往往是直观的...只是用词听起来很奇怪。如果没有对值类型和引用类型有一些直观的认识,没有人会在 C++ 中使用 3 天。

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

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

    【讨论】:

    • 然而,我认为你对值语义的解释有点不对劲。当然在a 上加3 和在b 上加3 一样,都是[1, 2, 3]。如果仅将 3 附加到a,它们当然具有不同的值,就像两个整数为 5 时仅将 1 加到其中一个时具有不同的值一样。事实上,容器实际上也有值语义。
    • @ChristianRau,这只是“当然”,因为你已经习惯了。请参阅 Python 以了解它的不同之处。
    • 我想我应该更详细一点:“将 3 附加到 a 与将 3 附加到 b 效果不同”应该更精确地写成,“如果您看到要附加的代码3 列出a,你不能盲目地用新代码替换那个代码,将3 附加到b 列表中并期望程序运行相同"
    【解决方案4】:

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

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

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

    您可能将“不变性”误认为是 const T 的意思——在您所说的问题中它们是相同的,但它们不是。

    【讨论】:

    • 一些非拥有的T const*和Parent使用shared_ptr&lt;const T&gt;的区别在于他实际上控制了所有权。所以他实际上可以保证创建的对象是常量。
    • @TemplateRex(如果我写 TRex,它会 ping 你吗?)——我不知道有任何方法可以动态分配 const 对象,并且 shared_ptr 不应该与自动一起使用或静态对象,因为它的重点是在引用计数为零时动态释放对象。
    • 不确定你在说什么:shared_ptr&lt;const T&gt; 是不是工作代码,或者它不是我认为的那样? (而且我认为只有自动完成的名称会 ping 我,就像 BVoigt 不会 ping 你一样 - 或者是吗?)
    • @TemplateRex:我想我只是从未将new 用于限定类型,但标准说它是合法的“[注意:type-id 可能是cv-qualified 类型,在这种情况下,由 new-expression 创建的对象具有 cv-qualified 类型。- end note ]"
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-08-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-10-09
    • 2015-03-06
    • 1970-01-01
    相关资源
    最近更新 更多