为什么复制常量shared_ptr和不违反常量?

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

本文关键字:常量 ptr 复制 shared 为什么      更新时间:2023-10-16

尽管我的代码编译得很好,但这一直困扰着我,我在stackoverflow上找不到答案。下面的泛型构造函数是将shared_ptr传递给构造函数中的类实例的一种方法。

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的使用次数,从而违反了所谓的constness

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

(对于任何想要实现它的人来说,请注意这可能是一个更好的实现)

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

shared_ptr(const shared_ptr& r) noexcept;

该标准规定(C++11 20.7.2.2.1/18"shared_ptr构造函数")"如果r为空,则构造一个空的shared_ptr对象;否则,构造一个与r共享所有权的shared_pt对象"。

该标准没有提及如何通过引用const来实现"与r共享所有权"。一些选项可能是:

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

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

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是const,获取副本也不需要对shared_ptr的内部状态进行任何更改。突变发生在控制块中,shared_ptr将指向

因此,合同没有被破坏。就像你申报一样

T* const p;

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

问题的简单答案是no,因为引用计数不是存储在共享指针实例中,而是存储在负责保持引用计数的外部对象中。当您复制构造shared_ptr时,引用计数会添加到外部对象中。看看Stephen T.Lavavej的这篇讲座,它解释了

const在接口中的实现,它意味着它想要的任何含义。其含义应记录在案。

通常,它意味着"ny状态的某个子集不会改变"。对于共享ptr,不能更改的状态子集是"我所指向的"。

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

在C++标准库中,const可以被解释为"线程安全"——因为如果const操作是线程安全的,并且您将它们放在std容器中,那么std容器就有线程安全的常量操作。

所谓线程安全,我的意思不是同步——我的意思是两个不同的线程,都在做const的事情,这是可以的。如果一个线程正在做非常量的事情,那么所有的赌注都会被取消

这允许简单的读写器锁定逻辑。

由于添加/删除ref确实是线程安全的,而重新封装ptr则不是。。。

我的问题如下:在我的理解中,声明这样的参数const。。。承诺不会改变t.

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

  1. 它有可变的变量——这些变量是在const约束下改变的,但设计方法论认为这些变量应该是罕见的,不应该是可观察的。它们的一个更常见的用途是缓存计算成本高昂的东西。因此,您有一个常量函数get,它进行大量计算以返回一个值——您希望对其进行优化,以便创建缓存。在get调用期间,缓存必须更改,但实际上get总是返回相同的东西,因此没有人可以观察到对象的状态已经更改。

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

shared_ptr作为引用传递不会增加其引用计数。此外,您在这里没有复制任何内容,您只是获取引用,因此引用计数保持不变。

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

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