智能指针模糊指向堆或栈对象

4

我的一个应用程序可以从 std::unique_ptr<T> 的变体中受益,该变体可以配置为不总是假定拥有指向的对象。

考虑以下类层次结构:

class AbstractFoo { ... };

template<typename T> Foo : public AbstractFoo 
{
    Foo( const AbstractFoo& absFoo ) { ... } 
    ... 
};

提供一个API,标准化每个例程都接受一个AbstractFoo并根据需要转换为特定的Foo<T>实例。如果对AbstractFoo的引用实际上已经是适当派生类型的实例,则仅需要进行dynamic_cast,不需要复制任何数据。但是,当抽象引用是不正确的类型时,需要执行非平凡工作以在请求的格式中创建副本。

我期望的接口如下:

template<typename T>
my_unique_ptr<Foo<T>> Convert( AbstractFoo& absFoo )
{
    if( Foo<T>* foo = dynamic_cast<Foo<T>*>(&absFoo) )
        return my_unique_ptr<Foo<T>>( foo, false );
    else
        return my_unique_ptr<Foo<T>>( new Foo<T>(absFoo) );
}

void Bar( AbstractFoo& absFoo )
{
    my_unique_ptr<Foo<T>> ptr = Convert<T>( absFoo );
    ...
}

在这里,类make_unique_ptr<T>有一个构造函数,与std::unique_ptr<T>相似,但它有一个可选的布尔参数,用于指定指针是否应该由智能指针拥有。

对于这种情况,是否有最佳实践解决方案?我希望避免返回原始指针,因为如果在手动删除对象之前抛出异常,可能会导致内存泄漏。


3
使用shared_ptr是否可行?(你需要让AbstractFoo实现enable_shared_from_this或将一个shared_ptr传递给Convert而不是引用。) - dlf
3
是的,这似乎是 shared_ptr 的一个适用场景,它恰好实现了你所说的:不总是假设所有权,并相应地行为。你的布尔标志在 shared_ptr 的引用计数器中有一个隐式等价物。另一种可能性是即使类型正确也始终克隆对象,但如果可能的话,提供一种便宜且快速的克隆方法。 - BartoszKP
1
@JackPoulson 是的,没错。如果这是无法接受的条件,你需要寻找其他解决方案(比如BartoszKP建议的始终创建克隆对象,但使克隆操作变得便宜)。 - dlf
4
使用一个空的删除器和智能指针怎么样? - programmerjake
@JackPoulson 幸运的是,删除器只是一个注入函数,而不是模板参数。这个事实使得各种有趣的事情成为可能。 - dlf
显示剩余6条评论
2个回答

4
您可以结合自定义删除器使用shared_ptr
template<typename T>
shared_ptr<Foo<T>> Convert( AbstractFoo& absFoo )
{
    if( Foo<T>* foo = dynamic_cast<Foo<T>*>(&absFoo) )
        return shared_ptr<Foo<T>>( foo, [](Foo<T>*){} ); // do-nothing deleter
    else
        return make_shared<Foo<T>>( absFoo ); // regular deleter
}

更新:显然,当我在打字时,programmerjake在评论中写了同样的想法。如果你想将其作为答案编写,我会删除我的答案。

只有在代码未能删除传递给“Convert”的原始“AbstractFoo&”时,它才会泄漏,这在以前也是如此。否则,引用计数将像往常一样上下变化,直到达到0为止,此时...什么都不会发生。 :) - dlf
@BartoszKP 不,返回的对象的生命周期与参数相同。 - programmerjake

2

我认为这个设计是脆弱的:

template<typename T>
my_unique_ptr<Foo<T>> Convert( AbstractFoo& absFoo )
{
    if( Foo<T>* foo = dynamic_cast<Foo<T>*>(absFoo) )
        return my_unique_ptr<Foo<T>>( foo, false );
    else
        return my_unique_ptr<Foo<T>>( new Foo<T>(absFoo) );
}
if 路径中,你创建了一个对象, absFoo 参数的生命周期绑定。

else 路径中,您创建了一个不与任何其他对象的生命周期绑定的对象。

调用者无法区分这两种情况 - 这似乎相当脆弱。


至于仍然使用它(像dlf所建议的一样使用shared_ptr)...... 也许将Convert命名为smart_foo_cast之类的名称,这样其名称中就更好地呈现出生命周期的事情了。

个人而言,我还希望它接受AbstractFoo*(这个更改不会影响外部API)。只需确保永远不要使用const AbstractFoo&,因为你永远不知道const&何时可能是隐式临时值。


如果您可以通过使用unique_ptr作为参数 "sink",或 "share" 参数来共享参数,则问题将消失。

// Caller always yields ownership of absFoo:
template<typename T>
unique_ptr<Foo<T>> Convert( unique_ptr<AbstractFoo> absFoo );

// Caller may yield ownership of absFoo:
// (Caller needs to check whether absFoo was moved-from)
template<typename T>
unique_ptr<Foo<T>> Convert( unique_ptr<AbstractFoo>& absFoo );

// Caller may share ownership of absFoo with return value:
template<typename T>
shared_ptr<Foo<T>> Convert( const shared_ptr<AbstractFoo>& absFoo );

“Convert”函数仅是库内部使用的实用函数。要求输入被智能指针包装会产生外部影响。 - Jack Poulson
此外,您提出了一个很好的观点,即不接受const引用输入,因此我已经修改了实际项目中的接口以接受指针。 - Jack Poulson

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