我可以将 shared_ptr<T> & 转换为 shared_ptr<T const> & 吗,而不改变 use_count?

8
我有一个程序使用了boost::shared_ptr,并特别依赖于use_count的准确性来执行优化。
例如,假设有两个名为lhs和rhs的参数指针进行加法操作。他们都具有类型shared_ptr<Node>。当进行加法操作时,我会检查use_count,如果发现其中一个参数的引用计数恰好为1,则将其重用以在原地执行操作。如果没有任何参数可以重用,则必须分配新的数据缓冲区,并在外部执行操作。我正在处理巨大的数据结构,因此原地优化非常有益。
由于这一点,我永远不能无原因复制shared_ptr,即每个函数通过引用或const引用传递shared_ptr以避免扭曲use_count
我的问题是:有时我有一个想要转换为shared_ptr<T const> &shared_ptr<T> &,但如何在不扭曲use count的情况下进行转换呢? static_pointer_cast返回一个新对象而不是引用。我倾向于认为可以直接转换整个shared_ptr,例如:
void f(shared_ptr<T> & x)
{
  shared_ptr<T const> & x_ = *reinterpret_cast<shared_ptr<T const> *>(&x);
}

我非常怀疑这不符合标准,但是如我所说,它可能会起作用。是否有一种可以保证安全和正确的方法来实现此操作?

更新以聚焦问题

批评设计并不能帮助回答这篇帖子。有两个有趣的问题需要考虑:

  1. 存在任何保证 (boost::shared_ptr 的作者或标准,在std::tr1::shared_ptr的情况下) shared_ptr<T>shared_ptr<T const> 具有相同的布局和行为吗?

  2. 如果(1)是真的,则上面的reinterpret_cast使用是合法的吗?我认为你很难找到一个生成失败代码的编译器,但这并不意味着它是合法的。无论你的答案是什么,您能在C++标准中找到支持吗?


使用引用计数的方式看起来像是一种非常奇特的设计路线。为什么不向用户公开一些控制操作是原地进行还是通过复制进行的控制权呢?或者分离共享和唯一指针呢? - Kerrek SB
4个回答

11
我有时会有一个`shared_ptr &`,我想将其转换为`shared_ptr &`,但是如何在不扭曲使用计数的情况下进行操作呢?
你不能这样做。这个概念本身就是错误的。考虑一下裸指针`T*`和`const T*`的情况。当你将`T*`强制转换为`const T*`时,你现在有了两个指针。你没有两个指向同一指针的引用;你有两个指针。
智能指针应该有什么不同?你有两个指针:一个指向`T`,一个指向`const T`。它们都共享同一个对象,所以你使用了两个指针。因此,你的`use_count`应该是2,而不是1。
你的问题在于试图重载`use_count`的含义,为某些其他目的占用其功能。简而言之:你做错了。
你对`use_count`为1的`shared_ptr`的描述...让人感到恐惧。你基本上是说某些函数占用了其中一个参数,而调用者显然正在使用它(因为调用者显然仍在使用它)。而且调用者不知道哪个被占用了(如果有的话),所以调用者不知道函数后参数的状态。修改参数以进行此类操作通常不是一个好主意。
此外,你所做的只有在通过引用传递`shared_ptr `时才能起作用,这本身就不是一个好主意(像常规指针一样,智能指针几乎总是按值传递)。
简而言之,你正在采取一种非常常见的对象,具有明确定义的习惯用法和语义,然后要求以几乎从不使用它们的方式使用它们,并且具有与每个人实际使用方式相反的专门化语义。那不是好事。
你实际上创建了可占用指针的概念,即可以处于3种使用状态的共享指针:空、仅由给予者使用,因此你可以从中窃取,以及被多个人使用,因此你不能拥有它。这不是`shared_ptr`存在的语义支持。因此,你应该编写自己的智能指针,在更自然的方式中提供这些语义。

有些东西可以识别指针实例和实际使用者之间的差异。这样,你就可以正确地按值传递它,但你还要有一种方式来表示你正在使用它,不希望这些其他函数声称拥有它。它可以在内部使用shared_ptr,但应该提供自己的语义。


我同意,为了允许重用临时对象,在C++11中正确的方式是使用移动语义来确定特定值(而不是值的' shared_ptr')是否是临时的,从而确定是否安全进行覆盖。 - Jeremiah Willcock
除非我误解了什么,否则我认为你把问题反了。通过引用获取指针会破坏对use_count的使用,而通过值获取指针应该可以工作。此外,对于某些情况,通过引用获取shared_ptrs是常见且推荐的。请参阅我的答案 - bames53
3
这段话的意思是:指针通常通过值而不是引用使用,无论是智能还是愚蠢的指针。大量使用 shared_ptr<T>& 不是一个好主意。如果一个函数声明拥有内存(因此应该通过值来传递 shared_ptr),或者它没有声明拥有内存(因此应该传递裸指针)。 - Nicol Bolas
@NicolBolas 我认为仅有调用者在使用的情况并不意味着可以随意占用它。我只会认为当没有其他人在使用该对象时,才可以随意占用该对象。但我想程序可能会选择不同的语义,我只是觉得这很令人惊讶。 - bames53
@bames53:我同意这让人感到惊讶,这就是为什么我反对它的原因。只有使用移动语义,才能通过值传递shared_ptr给函数,并且仍然只由一个用户拥有它,而不是一个临时对象。所以我认为他必须是通过引用来使用它。 - Nicol Bolas
显示剩余2条评论

7

static_pointer_cast是处理这个任务的正确工具,您已经确认了它。

问题不在于它返回一个新对象,而是它不会改变旧对象。您想摆脱非const指针并继续使用const指针。您真正想要的是static_pointer_cast< T const >( std::move( old_ptr ) )。但是rvalue引用没有重载。

解决方法很简单:像std::move一样手动使旧指针无效。

auto my_const_pointer = static_pointer_cast< T const >( modifiable_pointer );
modifiable_pointer = nullptr;

它可能比reinterpret_cast慢一点,但更有可能起作用。不要低估库实现的复杂性以及在滥用时可能发生的故障。

另外:使用pointer.unique()而不是use_count() == 1。一些实现可能使用没有缓存使用计数的链表,使use_count()为O(N),而unique测试仍然为O(1)。标准推荐使用unique进行写时复制优化。

编辑:现在我看到你提到了

我无法没有原因地复制shared_ptr,即每个函数通过引用或const引用接收shared_ptr以避免扭曲use_count

这样做是错误的。您在shared_ptr已经执行的所有权语义之上添加了另一层。它们应该按值传递,并在调用方不再需要所有权时使用std::move。如果分析器说您正在花费时间调整引用计数,则可以在内部循环中添加指针的引用。但通常情况下,如果您不能将指针设置为nullptr,因为您不再使用它,但其他人可能会使用它,则您真的失去了所有权。


1
如果你将一个shared_ptr转换为不同的类型,而不改变引用计数,这意味着你现在有两个指向相同数据的指针。因此,除非你删除旧指针,否则你不能使用shared_ptr进行这种操作,否则会“扭曲引用计数”。
我建议你在这里使用原始指针,而不是费尽心思地不使用shared_ptr的特性。如果你需要有时创建新的引用,请使用enable_shared_from_this来派生一个新的shared_ptr到一个现有的原始指针。

这并不意味着有两个指针。这是一个指针的两个引用,其中一个引用被类型欺骗了。我怀疑是否有一种安全的方法(因此提出问题),但令人沮丧的是,const限定符可能不会改变shared_ptr的布局或行为,并且在删除底层对象时被忽略。 - AndyJost
1
@AndyJost,添加 const 并不能保证 shared_ptr 的布局或行为不会改变。如果您正在手动管理所有权(这确实是您正在做的事情),只需使用原始指针将其传递给函数即可。 - bdonlan
顺便说一下,如果没有安全的指针转换方式,建议使用原始指针(实际上,我会使用引用)是正确的做法。 - AndyJost
保证是问题的关键。从先验上看,C++标准中并没有这样的保证。但是shared_ptr的作者们可以提供这样的保证。如果他们这样做了,在这种情况下将是一件很好的事情。 - AndyJost
1
@AndyJost,shared_ptr 的创建者无法做出这样的保证。您正在两种不同的原始类型之间进行转换,编译器不能保证 shared_ptr<const int>shared_ptr<int> 具有相同的表示形式,无论 shared_ptr 的作者做了什么。 - bdonlan
这只是愚蠢的想法。布局规则已经非常精确地说明了(为了C兼容性和许多其他原因)。当然可以确保shared_ptr<T>和shared_ptr<T const>在内存中具有相同的布局! - AndyJost

0
当进行加法操作时,我会检查use_count,如果发现其中一个参数的引用计数恰好为1,则会重复使用它来原地执行操作。

除非您在整个程序中应用其他规则使其成为有效的,否则这并不一定有效。请考虑:

shared_ptr<Node> add(shared_ptr<Node> const &lhs,shared_ptr<Node> const &rhs) {
    if(lhs.use_count()==1) {
        // do whatever, reusing lhs
        return lhs;
    }
    if(rhs.use_count()==1) {
        // do whatever, reusing rhs
        return rhs;
    }
    shared_ptr<Node> new_node = ... // do whatever without reusing lhs or rhs
    return new_node;
}

void foo() {
    shared_ptr<Node> a,b;
    shared_ptr<Node> c = add(a,b);

    // error, we still have a and b, and expect that they're unchanged! they could have been modified!
}

相反,如果您按值使用智能指针:

shared_ptr<Node> add(shared_ptr<Node> lhs,shared_ptr<Node> rhs) {

如果 use_count() == 1,则您知道您的副本是唯一的,可以安全地重用它。
然而,使用这作为优化存在问题,因为复制 shared_ptr 需要同步。很可能在所有这些地方进行同步的成本比通过重用现有 shared_ptr 节省的成本更高。所有这些同步是推荐非拥有 shared_ptr 的代码应将 shared_ptr 作为引用而不是值传递的原因。

拷贝 shared_ptr 只需要原子增减操作。正如你所指出的,如果你移动它们,那么甚至不需要这个。因此,按值接受它们仍然是可以的。如果你不打算声明对它们的所有权,那么你根本不应该接受 shared_ptr - Nicol Bolas
1
@NicolBolas 一些代码可能会有条件地拥有所有权,因此需要使用 shared_ptr。即使是原子增量和减量也不是免费的。这里是 Herb Sutter 和 Scott Meyers 对此的看法的片段:https://dev59.com/EHRC5IYBdhLWcg3wVvnL#8844924。 - bames53
1
@AndyJost,我只是在指出为什么你可能不应该使用use_count()来做这件事。 - bames53
1
@bames53 Herb Sutter和Stephan Lavavej在这里讨论了shared_ptr按引用或按值传递的问题:http://channel9.msdn.com/Events/GoingNative/GoingNative-2012/Interactive-Panel-Ask-Us-Anything-,并不是每个人都同意通过引用传递shared_ptr应该很常见。就个人而言,我同意Nicol的观点。如果代码没有使用任何shared_ptr功能,它应该只采用Node&或Node const&,调用者只需传递*ptr即可。为什么要强制使用shared_ptr来限制您的选择呢? - R. Martinho Fernandes
@NicolBolas 嗯,使用shared_ptr并不意味着不理解程序的所有权图,但是理解所有权的方法不是遍历所有代码并查看谁在哪里传递什么。shared_ptr是一种RAII类型,看到它被传递应该告诉读者任何RAII类型被传递时会发生什么,即本地代码将这些资源的管理留给了RAII对象。了解谁拥有什么以及确保整体所有权图是DAG留给其他设备。 - bames53
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接