5

有没有办法在 C++ 中手动增加和减少 shared_ptr 的计数?

我试图解决的问题如下。我正在用 C++ 编写一个库,但接口必须是纯 C。在内部,我想使用 shared_ptr 来简化内存管理,同时保留通过 C 接口传递原始指针的能力。

当我通过接口传递一个原始指针时,我想增加引用计数。然后,客户端将负责调用一个函数,该函数将在不再需要传递的对象时减少引用计数。

4

7 回答 7

10

也许您正在跨 DLL 边界使用 boost::shared_ptr,这将无法正常工作。在这种情况下boost::intrusive_ptr可能会帮助你。这是滥用shared_ptr人们试图解决肮脏黑客的常见情况......也许我在你的情况下是错的,但应该没有充分的理由去做你想做的事情;-)

添加 07/2010:问题似乎更多来自 DLL 加载/卸载,而不是来自 shared_ptr 本身。即使是 boost 基本原理也没有说明什么时候boost::intrusive_ptr应该优先考虑的情况shared_ptr。我切换到 .NET 开发并且没有关注 TR1 关于这个主题的详细信息,所以请注意这个答案现在可能不再有效......

于 2009-09-27T05:46:39.560 回答
6

在你的建议

然后客户端将负责递减计数器。

意味着有问题的客户负责内存管理,并且您信任她。我还是不明白为什么。

实际修改 shared_ptr 计数器是不可能的......(嗯,我将在最后解释如何......)但还有其他解决方案。

解决方案1:完全所有权归客户

将指针移交给客户端(shared_ptr::release)并期望它在回调时将所有权传回给您(或者如果对象不是真正共享的,则简单地删除它)。

这实际上是处理原始指针时的传统方法,它也适用于此。缺点是你实际上只释放了这个 shared_ptr 的所有权。如果该对象实际上是共享的,那可能会带来不便……请耐心等待。

解决方案 2:使用回调

此解决方案意味着您始终保持所有权,并负责在客户需要时保持该对象的活动(和启动)。当客户端处理完对象后,您希望她告诉您,并在您的代码中调用一个回调,该回调将执行必要的清理。

struct Object;

class Pool // may be a singleton, may be synchronized for multi-thread usage
{
public:
  int accept(boost::shared_ptr<Object>); // adds ptr to the map, returns NEW id
  void release(int id) { m_objects.erase(id); }

private:
  std::map< int, boost::shared_ptr<Object> > m_objects;
}; // class Pool

这样,您的客户端“递减”计数器实际上是您的客户端使用您使用的 id 调用回调方法,并且您删除了一个 shared_ptr :)

黑客提升::shared_ptr

正如我所说的(因为我们在 C++ 中)实际上侵入 shared_ptr 是可能的。甚至有几种方法可以做到这一点。

最好的方法(也是最简单的)就是简单地将文件复制到另一个名称(my_shared_ptr ?),然后:

  • 更改包含警卫
  • 在开头包含真正的 shared_ptr
  • 用您自己的名称重命名 shared_ptr 的任何实例(并将私有更改为公共以访问属性)
  • 删除所有已经在真实文件中定义的东西以避免冲突

通过这种方式,您可以轻松获得自己的 shared_ptr,您可以访问它的计数。它并没有解决让 C 代码直接访问计数器的问题,您可能必须在此处“简化”代码以将其替换为内置的(如果您不是多线程的,这可以工作,并且是彻头彻尾的灾难性如果你是)。

我故意省略了“reinterpret_cast”技巧和指针偏移了。有很多方法可以非法访问 C/C++ 中的某些内容!

我可以建议你不要使用这些黑客吗?我上面介绍的两种解决方案应该足以解决您的问题。

于 2009-10-01T18:22:54.447 回答
2

您应该在这里进行关注点分离:如果客户端传入原始指针,则客户端将负责内存管理(即事后清理)。如果您创建指针,您将负责内存管理。这也将帮助您解决另一个答案中提到的 DLL 边界问题。

于 2009-09-28T14:27:55.717 回答
2

1. 把手?

如果您想要最大的安全性,请给用户一个句柄,而不是指针。这样一来,他是不可能free一试就成功的。

我将在下面假设,为简单起见,您将为用户提供对象指针。

2.获取和取消获取?

您应该创建一个经理类,如 Matthieu M. 在他的回答中所描述的,以记住用户获得/未获得的内容。

由于接口是 C,你不能指望他使用delete什么的。所以,像这样的标题:

#ifndef MY_STRUCT_H
#define MY_STRUCT_H

#ifdef __cplusplus
extern "C"
{
#endif // __cplusplus

typedef struct MyStructDef{} MyStruct ; // dummy declaration, to help
                                        // the compiler not mix types

MyStruct * MyStruct_new() ;
size_t     MyStruct_getSomeValue(MyStruct * p) ;
void       MyStruct_delete(MyStruct * p) ;

#ifdef __cplusplus
}
#endif // __cplusplus

#endif // MY_STRUCT_H

将使用户能够使用您的类。我使用了一个虚拟结构的声明,因为我想通过不强制他使用泛型void *指针来帮助 C 用户。但是使用void *仍然是一件好事。

实现该功能的 C++ 源代码是:

#include "MyClass.hpp"
#include "MyStruct.h"

MyManager g_oManager ; // object managing the shared instances
                       // of your class

extern "C"
{

MyStruct * MyStruct_new()
{
   MyClass * pMyClass = g_oManager.createMyClass() ;
   MyStruct * pMyStruct = reinterpret_cast<MyStruct *>(pMyClass) ;
   return pMyStruct ;
}

size_t MyStruct_getSomeValue(MyStruct * p)
{
   MyClass * pMyClass = reinterpret_cast<MyClass *>(p) ;

   if(g_oManager.isMyClassExisting(pMyClass))
   {
      return pMyClass->getSomeValue() ;
   }
   else
   {
      // Oops... the user made a mistake
      // Handle it the way you want...
   }

   return 0 ;
}

void MyStruct_delete(MyStruct * p)
{
   MyClass * pMyClass = reinterpret_cast<MyClass *>(p) ;
   g_oManager.destroyMyClass(pMyClass) ;
}

}

请注意,指向 MyStruct 的指针是完全无效的。如果不将其重新解释为原始 MyClass 类型,则不应出于任何原因使用它(有关更多信息,请参见 Jaif 的答案。C 用户将仅将其与关联的 MyStruct_* 函数一起使用。

另请注意,此代码验证该类确实存在。这可能有点矫枉过正,但可能会使用经理(见下文)

3. 关于经理

正如 Matthieu M. 所建议的那样,管理器将持有一个包含共享指针作为值(以及指针本身或句柄作为键)的映射。或者一个多图,如果用户有可能以某种方式多次获取同一个对象。

使用管理器的好处是,您的 C++ 代码将能够跟踪用户未正确“未获取”哪些对象(在获取/取消获取方法中添加信息,例如__FILE____LINE__有助于缩小错误搜索范围)。

因此,经理将能够:

  1. 不释放一个不存在的对象(顺便说一句,C 用户是如何设法获得一个对象的?)
  2. 在执行结束时知道哪些对象不是未被获取的
  3. 如果是未获得的对象,无论如何都要销毁它们(从 RAII 的角度来看这是好的)这有点邪恶,但你可以提供这个
  4. 如上面的代码所示,它甚至可以帮助检测未指向有效类的指针
于 2010-07-11T12:05:05.607 回答
1

我遇到了一个用例,我确实需要这样的东西,与 IOCompletionPorts 和并发问题有关。骇人听闻但符合标准的方法是按照Herb Sutter在这里的描述对其进行辩护。

以下代码片段适用于 VC11 实现的 std::shared_ptr:

实施文件:

namespace {
    struct HackClass {
        std::_Ref_count_base *_extracted;
    };
}

template<>
template<>
void std::_Ptr_base<[YourType]>::_Reset<HackClass>(std::auto_ptr<HackClass> &&h) {
     h->_extracted = _Rep; // Reference counter pointer
}

std::_Ref_count_base *get_ref_counter(const std::shared_ptr<[YourType]> &p) {
     HackClass hck;
     std::auto_ptr<HackClass> aHck(&hck);

     const_cast<std::shared_ptr<[YourType]>&>(p)._Reset(std::move(aHck));

     auto ret = hck._extracted; // The ref counter for the shared pointer
                                // passed in to the function

     aHck.release(); // We don't want the auto_ptr to call delete because
                     // the pointer that it is owning was initialized on the stack

     return ret;
}

void increment_shared_count(std::shared_ptr<[YourType]> &sp) {
     get_ref_counter(sp)->_Incref();
}

void decrement_shared_count(std::shared_ptr<[YourType]> &sp) {
     get_ref_counter(sp)->_Decref();
}

将 [YourType] 替换为您需要修改计数的对象类型。重要的是要注意,这非常 hacky,并且使用平台特定的对象名称。你必须经历的工作量才能获得这个功能可能表明它是一个多么糟糕的想法。另外,我正在使用 auto_ptr 玩游戏,因为我从 shared_ptr 劫持的函数需要一个 auto_ptr。

于 2013-05-27T23:32:14.947 回答
1

另一种选择是动态分配 shared_ptr 的副本,以增加 refcount,并释放它以减少它。这保证了我的共享对象在 C api 客户端使用时不会被破坏。

在下面的代码片段中,我使用 increment() 和 decrement() 来控制 shared_ptr。为简单起见,我将初始 shared_ptr 存储在全局变量中。

#include <iostream>
#include <boost/shared_ptr.hpp>
#include <boost/make_shared.hpp>
#include <boost/scoped_ptr.hpp>
using namespace std;

typedef boost::shared_ptr<int> MySharedPtr;
MySharedPtr ptr = boost::make_shared<int>(123);

void* increment()
{
    // copy constructor called
    return new MySharedPtr(ptr);
}

void decrement( void* x)
{
    boost::scoped_ptr< MySharedPtr > myPtr( reinterpret_cast< MySharedPtr* >(x) );
}

int main()
{
    cout << ptr.use_count() << endl;
    void* x = increment();
    cout << ptr.use_count() << endl;
    decrement(x);
    cout << ptr.use_count() << endl;

    return 0;
}

输出:

1
2
1

于 2016-03-23T07:40:22.510 回答
0

最快的并发无锁管理器(如果您知道自己在做什么)。

template< class T >
class shared_pool
{
public:

    typedef T value_type;
    typedef shared_ptr< value_type > value_ptr;
    typedef value_ptr* lock_handle;

shared_pool( size_t maxSize ):
    _poolStore( maxSize )
{}

// returns nullptr if there is no place in vector, which cannot be resized without locking due to concurrency
lock_handle try_acquire( const value_ptr& lockPtr ) {
    static value_ptr nullPtr( nullptr );
    for( auto& poolItem: _poolStore ) {
        if( std::atomic_compare_exchange_strong( &poolItem, &nullPtr, lockPtr ) ) {             
            return &poolItem;
        }
    }
    return nullptr;
}


lock_handle acquire( const value_ptr& lockPtr ) {
    lock_handle outID;
    while( ( outID = try_acquire( lockPtr ) ) == nullptr ) {
        mt::sheduler::yield_passive(); // ::SleepEx( 1, false );
    }
    return outID;
}

value_ptr release( const lock_handle& lockID ) {
    value_ptr lockPtr( nullptr );
    std::swap( *lockID, lockPtr);
    return lockPtr;
}

protected:

    vector< value_ptr > _poolStore;

};

std::map 不是那么快,需要额外的搜索、额外的内存、自旋锁定。但它通过手柄方法提供了额外的安全性。

顺便说一句,手动发布/获取的破解似乎是一种更好的方法(在速度和内存使用方面)。C++ std 最好在他们的类中添加这样的功能,只是为了保持 C++ 剃刀形状。

于 2013-09-18T23:26:10.810 回答