9

在这篇富有洞察力的文章中,一位 Qt 程序员试图解释 Qt 实现的不同类型的智能指针。一开始,他区分了共享数据和共享指针本身:

首先,让我们弄清楚一件事:共享指针和共享数据是有区别的。当您共享指针时,指针的值及其生命周期受到智能指针类的保护。换句话说,指针是不变量。但是,指针指向的对象完全不受其控制。我们不知道对象是否可复制,是否可分配。

现在,数据共享涉及到智能指针类了解有关被共享数据的一些信息。事实上,关键在于数据是共享的,我们不在乎如何共享。在这一点上,指针被用来共享数据的事实是无关紧要的。例如,您并不真正关心 Qt 工具类是如何隐式共享的,是吗?对您而言重要的是它们是共享的(从而减少了内存消耗)并且它们就像不共享一样工作。

坦率地说,我只是不理解这个解释。文章评论中有一个澄清请求,但我发现作者的解释不够充分。

如果你明白这一点,请解释一下。这种区别是什么,其他共享指针类(即来自 boost 或新的 C++ 标准)如何适应这种分类法?

提前致谢

4

3 回答 3

7

在稍后的评论中,他稍微澄清了一点

这是我在第一部分中试图理解的重点。当您使用 QSharedPointer 时,您共享指针的所有权。该类只控制和处理指针——其他任何东西(如对数据的访问)都超出了它的范围。当您使用 QSharedDataPointer 时,您正在共享数据。并且该类旨在用于隐式共享:因此它可能会分裂。

试图解释:

需要注意的是,这里的“指针”并不是指存储地址的对象,而是指对象所在的存储位置(地址本身)。我认为,严格来说,您必须说您正在共享地址。boost::shared_ptr因此是共享“指针”的智能指针。boost::intrusive_ptr或另一个侵入式智能指针似乎也共享该指针,尽管知道有关指向的对象的某些信息(它具有引用计数成员或递增/递减它的函数)。

例子:如果有人和你共享一个黑盒子,他不知道黑盒子里有什么,这类似于共享指针(代表盒子),而不是共享数据(盒子里面有什么)。事实上,你甚至不知道盒子里面的东西是可共享的(如果盒子里什么都没有呢?)。智能指针由您和其他人表示(当然,您不是共享的),但地址是盒子,是共享的。

共享数据意味着智能指针足够了解指向的数据,它可能会更改指向的地址(这需要复制数据等)。因此,指针现在可能指向不同的地址。由于地址不同,地址不再共享。这也是std::string在某些实现上所做的:

std::string a("foo"), b(a);
 // a and b may point to the same storage by now.
std::cout << (void*)a.c_str(), (void*)b.c_str();
 // but now, since you could modify data, they will
 // be different
std::cout << (void*)&a[0], (void*)&b[0];

共享数据并不一定意味着您有一个指针呈现给您。您可以std::string通过纯粹的方式使用 aa[0]并且cout << a;永远不要接触任何c_str()功能。仍然共享可能会在幕后进行。许多 Qt 类和其他小部件工具包的类也会发生同样的情况,这称为隐式共享(或写时复制)。所以我认为可以这样总结:

  • 共享指针:当我们复制一个智能指针时,我们总是指向同一个地址,这意味着我们共享指针值。
  • 共享数据:我们可能会在不同的时间指向不同的地址。这意味着我们知道如何将数据从一个地址复制到另一个地址。

所以试图分类

  • boost::shared_ptr, boost::intrusive_ptr: 共享指针,而不是数据。
  • QString, QPen, QSharedDataPointer: 共享它包含的数据。
  • std::unique_ptr, std::auto_ptr(还有QScopedPointer):既不共享指针,也不共享数据。
于 2010-04-17T13:05:30.323 回答
3

假设我们有这门课

struct BigArray{
   int  operator[](size_t i)const{return m_data[i];}
   int& operator[](size_t i){return m_data[i];}
private:
   int m_data[10000000];
};

现在说我们有两个实例:

BigArray a;
a[0]=1;//initializaation etc
BigArray b=a;

此时我们想要这个不变量

assert(a[0]==b[0]);

默认的复制 ctor 确保了这种不变性,但是以深度复制整个对象为代价。我们可能会尝试这样的加速

struct BigArray{
   BigArray():m_data(new int[10000000]){}
   int  operator[](size_t i)const{return (*m_data)[i];}
   int& operator[](size_t i){return (*m_data)[i];}
private:
   shared_ptr<int> m_data;
};

这也将满足不变量,无需进行深拷贝,所以到目前为止一切都很好。现在使用我们所做的这个新实现

b[0]=2;

现在我们希望它与深拷贝案例 assert(a[0]!=b[0]); 一样工作。但它失败了。为了解决这个问题,我们需要稍作改动:

struct BigArray{
       BigArray():m_data(new int[10000000]){}
       int  operator[](size_t i)const{return (*m_data)[i];}
       int& operator[](size_t i){
          if(!m_data.unique()){//"detach"
            shared_ptr<int> _tmp(new int[10000000]);
            memcpy(_tmp.get(),m_data.get(),10000000);
            m_data=_tmp;
          }
          return (*m_data)[i];
       }
    private:
       shared_ptr<int> m_data;
    };

现在我们有了一个类,它在只需要 const 访问时进行浅拷贝,而在需要非常量访问时进行深拷贝。这就是“shared_data”指针概念背后的想法。const调用不会进行深度复制(他们称其为“分离”),而非 const 调用将在共享时进行深度复制。它还在 operator== 之上添加了一些语义,因此它不仅比较指针,而且还比较数据,这样就可以了:

BigArray b=a;//shallow copy
assert(a==b);//true
b[0]=a[0]+1;//deep copy
b[0]=a[0];//put it back
assert(a==b);//true

这种技术称为 COW(写时复制),自 C++ 出现以来就一直存在。它也非常脆弱——上面的例子似乎很有效,因为它很小并且用例很少。在实践中它很少值得麻烦,事实上 C++0x 已经弃用了 COW 字符串。所以谨慎使用。

于 2010-04-17T13:58:11.420 回答
1

在第一种情况下,您向指针添加了一个间接级别,以便智能指针表示的对象包装原始指针。只有一个指向对象的指针,而包装器的工作就是跟踪对原始指针的引用。一段非常简单的代码可能如下所示:

template<typename T>
struct smart_ptr {
    T    *ptr_to_object;
    int  *ptr_to_ref_count;
};

当您复制结构时,您的复制/分配代码将必须确保引用计数增加(如果对象被破坏,则减少),但指向实际包装对象的指针永远不会改变,并且可以只是浅复制。由于结构非常小,因此复制起来既简单又便宜,“所有”你要做的就是操纵引用计数。

在第二种情况下,它对我来说更像是一个对象存储库。“隐式共享”部分表明您可能会FooWidget通过执行类似的操作来向框架请求 a BarFoo.getFooWidget(),即使它看起来像指针 - 智能与否 - 您返回的指针是指向新对象的指针,您实际上是被交给了指向保存在某种对象缓存中的现有对象的指针。从这个意义上说,它可能更类似于通过调用工厂方法获得的类似 Singleton 的对象。

至少这就是我听起来的区别,但我可能离题太远了,我需要谷歌地图才能找到回去的路。

于 2010-04-17T08:50:26.040 回答