【问题标题】:Can I cast shared_ptr<T> & to shared_ptr<T const> & without changing use_count?我可以在不更改 use_count 的情况下将 shared_ptr<T> & 转换为 shared_ptr<T const> & 吗?
【发布时间】:2012-04-15 03:51:37
【问题描述】:

我有一个使用boost::shared_ptrs 的程序,特别是依赖use_count 的准确性来执行优化。

例如,想象一个带有两个参数指针的加法运算,称为 lhs 和 rhs。假设他们都有shared_ptr&lt;Node&gt; 类型。当需要执行加法时,我会检查use_count,如果我发现其中一个参数的引用计数正好为1,那么我将重用它来执行操作。如果两个参数都不能重用,我必须分配一个新的数据缓冲区并就地执行操作。我正在处理庞大的数据结构,因此就地优化非常有益。

因此,我永远不能无故复制shared_ptrs,即每个函数都通过引用或常量引用来获取shared_ptrs,以避免扭曲use_count

我的问题是:我有时有一个shared_ptr&lt;T&gt; &amp;,我想将其转换为shared_ptr&lt;T const&gt; &amp;,但是我怎样才能在不扭曲使用计数的情况下做到这一点? static_pointer_cast 返回一个新对象而不是引用。我倾向于认为只投射整个shared_ptr 会起作用,例如:

void f(shared_ptr<T> & x)
{
  shared_ptr<T const> & x_ = *reinterpret_cast<shared_ptr<T const> *>(&x);
}

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

更新以聚焦问题

批评设计无助于回答这篇文章。有两个有趣的问题需要考虑:

  1. 是否保证shared_ptr&lt;T&gt;shared_ptr&lt;T const&gt; 具有相同的布局和行为?

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

【问题讨论】:

  • 以这种方式使用引用计数看起来像一个非常独特的设计路线。为什么不公开对操作是就地发生还是通过复制给用户进行一些控制?还是分开共享指针和唯一指针?

标签: c++ casting shared-ptr


【解决方案1】:

我有时有一个shared_ptr&lt;T&gt; &amp;,我想将其转换为shared_ptr&lt;T const&gt; &amp;,但我怎样才能在不扭曲使用计数的情况下做到这一点?

你没有。这个概念是错误的。考虑一下裸指针T*const T* 会发生什么。当您将T* 转换为const T* 时,您现在有两个指针。您没有对同一个指针的两个引用;你有两个指针。

为什么智能指针应该有所不同?您有两个指针:一个指向T,一个指向const T。它们都共享同一个对象的所有权,因此您正在使用其中的 两个。因此,您的 use_count 应该是 2,而不是 1。

您的问题是您试图重载use_count 的含义,将其功能用于其他目的。简而言之:你做错了。

你对shared_ptrs 的use_count 所做的事情的描述是......可怕。您基本上是在说某些函数选择了它的参数之一,调用者显然正在使用它(因为调用者显然仍在使用它)。并且调用者不知道声明了哪一个(如果有的话),所以调用者不知道函数之后参数的状态是什么。修改此类操作的参数通常不是一个好主意。

另外,只有当你通过引用传递shared_ptr&lt;T&gt; 时,你正在做的事情才能工作,这本身不是一个好主意(就像常规指针一样,智能指针应该几乎总是按值获取)。

简而言之,您正在使用具有明确定义的习语和语义的非常常用的对象,然后要求以几乎从未使用过的方式使用它,并具有专门的语义与每个人实际使用它们的方式背道而驰。这可不是什么好事。

您已经有效地创建了 co-optable 指针的概念,这是一个可以处于 3 种使用状态的共享指针:空、仅由提供给您的人使用,因此您可以从中窃取,以及正在使用不止一个人,所以你不能拥有它。这不是 shared_ptr 存在支持的语义。因此,您应该编写自己的智能指针,以更自然的方式提供这些语义。

能够识别您拥有的指针实例数量与实际用户数量之间的差异。这样,您可以正确地按值传递它,但是您可以通过某种方式说您当前正在使用它并且不希望这些其他功能之一声明它。它可以在内部使用shared_ptr,但它应该提供自己的语义。

【讨论】:

  • 我同意;为了允许重用临时对象,正确的方法是使用 C++11 中的移动语义来确定特定值(不是 shared_ptr 到某个值)是否是临时的,从而确定覆盖是否安全。
  • 除非我误解了某些事情,否则我认为您的问题已经逆转。通过引用获取指针会破坏use_count 的这种用法,而在按值获取指针时它应该可以工作。参考 shared_ptrs 也是很常见的,建议在某些情况下使用。见我的answer
  • 这个答案混淆了这个问题。 T * 与 T const * 的类比是不正确的;正确的类比是 T * & 和 T const * &。
  • @AndyJost:这就是重点。您最后一次使用 reference 指向指针是什么时候?指针,无论是聪明的还是愚蠢的,通常由 value 使用,而不是引用。所有这些shared_ptr&lt;T&gt;&amp;s 到处都不是一个好主意。一个函数要么声明内存的所有权(因此应该采用shared_ptr 的值),要么它没有,因此应该采用一个裸指针。
  • @bames:我认为他的添加不会基于您是否将最后一个 shared_ptr 移动到对象中(或者只是新分配的临时对象)。他没有声称使用 C++11,而是使用boost::shared_ptr。所以很可能他正在传递引用,期望use_count==1 意味着只有调用者在使用它,并且可以选择它。我不能确定,但​​这是我对他的描述的解释..
【解决方案2】:

static_pointer_cast 是适合这项工作的工具 - 您已经确定了这一点。

它的问题不在于它返回一个新对象,而是它使旧对象保持不变。您想摆脱非常量指针并继续使用 const 指针。你真正想要的是static_pointer_cast&lt; T const &gt;( std::move( old_ptr ) )。但是右值引用没有重载。

解决方法很简单:像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_ptrs,即每个函数都通过引用或常量引用来获取shared_ptrs,以避免扭曲use_count

这是做错了。您在 shared_ptr 已经做的基础上添加了另一层所有权语义。它们应该按值传递,在调用者不再需要所有权的地方使用std::move如果分析器说您正在花时间调整引用计数,那么您可能会在内部循环中添加一些引用指针。但作为一般规则,如果您不能设置指向 nullptr 的指针,因为您不再使用它,但其他人可能会使用它,那么您就真的失去了所有权。

【讨论】:

    【解决方案3】:

    如果您将shared_ptr 转换为不同的类型,而不更改引用计数,这意味着您现在将有两个指向相同数据的指针。因此,除非您删除旧指针,否则您无法在不“扭曲引用计数”的情况下使用 shared_ptrs 执行此操作。

    我建议您在这里改用原始指针,而不是竭尽全力不使用shared_ptrs 的功能。如果您有时需要创建新的引用,请使用enable_shared_from_this 将新的shared_ptr 派生到现有的原始指针。

    【讨论】:

    • 这不等于说有两个指针。这是对一个指针的两个引用,其中一个引用是类型欺骗的。怀疑有一种安全的方法(因此提出了问题),但它令人沮丧地接近,因为 const 限定符可能不会改变 shared_ptr 的布局或行为,并且在删除底层对象时被忽略。
    • @AndyJost,不能保证添加 const 不会改变 shared_ptr 的布局或行为。如果您手动管理所有权(这确实是您正在做的事情),只需使用原始指针传递给函数。
    • 顺便说一句,建议使用原始指针(实际上,我会使用引用)是正确的做法,缺少一种安全的指针转换方式。
    • 保证是问题的关键。先验地,C++ 标准中没有这样的保证。但是 shared_ptr 的作者可以做出这样的保证。如果他们这样做了,在这种情况下,这将是一件好事。
    • @AndyJost,shared_ptr 的创建者不能做出这样的保证。您在两种不同的原始类型之间进行转换,并且编译器不保证shared_ptr&lt;const int&gt;shared_ptr&lt;int&gt; 具有相同的表示,无论shared_ptr 作者做什么。
    【解决方案4】:

    到了执行加法的时候,我会检查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_ptrs 所节省的成本。所有这些同步都是建议不获取 shared_ptr 所有权的代码应该通过引用而不是值来获取 shared_ptr 的原因。

    【讨论】:

    • 复制shared_ptrs 只需要原子增量和减量。而且,正如您所指出的,如果您移动它们,那么它甚至不需要这样做。因此,按价值计算它们仍然可以。如果您不打算声明对它们的所有权,那么您就不应该使用shared_ptrs
    • 这种分析适用于任何程序......那又如何?
    • @NicolBolas 某些代码可能有条件地取得所有权,因此有必要取得 shared_ptr。即使是原子增量和减量也不是免费的。 Here's Herb Sutter 和 Scott Meyers 对此的评论。
    • @AndyJost 我只是指出为什么您可能不应该为此使用use_count()
    • @bames53 Herb Sutter 和 Stephan Lavavej 在这里也讨论了 shared_ptr by-ref 或 by-value 问题:channel9.msdn.com/Events/GoingNative/GoingNative-2012/… 并不是每个人都同意通过 shared_ptr by-ref 应该很常见。就个人而言,我同意尼科尔的观点。如果代码不使用任何shared_ptr 功能,它应该只使用Node&amp;Node const&amp;,调用者只需传递*ptr。为什么要通过强制使用shared_ptr 来限制您的选择?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-10-20
    • 2012-07-22
    • 1970-01-01
    • 1970-01-01
    • 2017-07-18
    • 2013-07-21
    • 1970-01-01
    相关资源
    最近更新 更多