为什么复制 const shared_ptr& 不违反 const-ness?

Why does copying a const shared_ptr& not violate const-ness?

尽管我的代码编译得很好,但这一直困扰着我,我无法在 Whosebug 上找到答案。以下通用构造函数是将 shared_ptr 传递给构造函数中的 class 实例的一种方法。

MyClass {
  MyClass(const std::shared_ptr<const T>& pt);
  std::shared_ptr<const T> pt_;  //EDITED: Removed & typo
};

MyClass::MyClass(const std::shared_ptr<const T>& pt)
  : pt_(pt)
{ }

这编译得很好。我的问题如下:在我的理解中,像这样声明一个参数 const:

void myfunc(const T& t)

承诺不更改 t。 但是,通过将 shared_ptr pt 复制到 pt_,我是否没有有效地增加 shared_ptr pt 的使用次数,从而违反了假定的 const-ness?

这可能是我对 shared_ptr 的根本误解?

(对于阅读本文并希望实施它的任何人,请注意 this 可能是更好的实施方式)

My question is the following: In my understanding, declaring a parameter const like this...promises not to change t.

不完全正确。承诺不会改变其任何可观察到的状态……大多数时候。 const 对象可以通过多种方式 "change":

  1. 它有可变变量——这些变量意味着在 const 约束下发生变化,但设计方法表明这些变量应该很少见并且不应该被观察到。它们的一个更常见的用途是缓存一些计算成本很高的东西。因此,您有一个 const 函数 get,它对 return 一个值进行大量计算——您希望对其进行优化,以便创建缓存。缓存必须在 get 调用期间更改,但实际上 get 总是 return 相同的事情,因此没有人可以观察到对象的状态已更改。

  2. 它有非常量指针或对其他对象的引用。在这些情况下,聚合,改变的不是对象,而是其他东西。这就是 shared_ptr 的情况,它有一个指向共享引用计数对象的指针,该对象实际上保存了指针的值。一开始这并不直观,因为此类对象的报告状态可能会发生变化,但实际上并不是对象本身发生了变化。这里的设计方法是根据具体情况而定,但除非您将指针声明为指向 const 的指针,否则该语言绝不会保护您。

传递一个shared_ptr作为引用不会增加它的引用count.Moreover,你在这里没有复制任何东西,你只是引用,所以引用计数保持不变。

请注意,使用对共享指针的引用作为 class 成员通常不是您想要的。通过这种方式,您无法保证指针在您想要使用它时仍然存在 - 这基本上是使用共享指针时的目标。

对您的编辑的回应: 现在,通过使用共享指针成员,您确实创建了一个副本,从而增加了引用计数。这是可能的,因为您可以复制 const 对象。所以你提到的这个保证不存在——本质上,关键字 mutable 阻止了它。

std::shared_prt<> 必须拥有的成员之一是老式的复制构造函数:

shared_ptr(const shared_ptr& r) noexcept;

标准说 (C++11 20.7.2.2.1/18 "shared_ptr constructors") "if r is empty, constructs an empty shared_ptr object; otherwise, constructs a shared_ptr object that shares ownership with r".

该标准没有提及如何通过 const 参考来实现 "shares ownership with r"。一些选项可能是:

  • 实现共享所有权语义的私有成员可能被标记为 mutable
  • 实现共享所有权的数据结构可能实际上并不存在于 shared_ptr 对象中 - 它们可能是一组单独的对象,例如可以通过指针获得。

const,在一个界面中,想表达什么就表达什么。它的含义应该被记录下来。

通常表示"some subset of ny state will not change"。对于shared ptr,不能改变的状态子集是"what I point to".

计数可以改变。内容可以更改。

在 C++ std 库中,const 可以解释为 "thread safe" —— 因为如果 const 操作是线程安全的,而你将它们放在 std容器,std 容器又具有线程安全的常量操作。

我所说的线程安全并不是指同步——我的意思是两个不同的线程,都在做常量的事情,没问题。如果一个线程正在做非常量的事情,那么所有的赌注都会被取消。

这允许简单的 reader-writer 锁逻辑。

并且因为 add/remove ref 确实是线程安全的,而重新安装 ptr 不是...

您问题的简单答案是,因为引用计数不存储在共享指针实例中,而是存储在负责保持引用计数的外部对象中.当你复制构造 a shared_ptr 时,引用计数被添加到外部对象中。查看 Stephen T. Lavavej 的 this lecture,其中解释了实现

共享指针的概念布局如下:

shared_ptr 包含指向对象的指针以及指向控制块的指针。控制指针对象生命周期的是控制块,而不是 shared_ptr 对象本身,它只不过是一个包装器和一些代码来通知控制块引用的数量已经增加或减少。它是存储引用计数、删除器和指向指向对象原始接口的指针的控制块(因此删除器可以针对正确的接口进行删除,即使已经进行了指针转换)。

* shared_ptr object *
| pointer to object | ---------------> object
| pointer to control block |----+   +> (possibly different interface
                                |   |   but still same object)
                                |   |
* control block *    <----------+   |
| reference count   |               |
| deleter           |               |
| pointer to object | --------------+

因为 shared_ptr 的记忆看起来像这样:

template<class T>
struct shared_ptr {
    T* ptr;
    control_block* pctrl;
};

应该开始变得明显的是,即使 shared_ptr 是常量,获取副本也不需要对 shared_ptr 的内部状态进行任何更改。突变发生在控制块中,由shared_ptr指向

因此契约没有被破坏。就像你声明

T* const p;

修改p本身是不可能的,但是修改(*p)是完全合理的。