unique_ptr的静态指针转换替代方法

44

我了解使用unique_ptrstatic_pointer_cast会导致所包含数据的共享所有权。
换句话说,我的意图是:

我理解使用unique_ptrstatic_pointer_cast会导致所包含数据的共享所有权。
换言之,我想要做的是:

unique_ptr<Base> foo = fooFactory();
// do something for a while
unique_ptr<Derived> bar = static_unique_pointer_cast<Derived>(foo);

无论如何这样做都会导致两个unique_ptr同时存在,因此它被严格禁止。
没错,这是有道理的,确实不存在像static_unique_pointer_cast一样的东西。

到目前为止,在我需要存储那些基类指针但又需要将它们转换为某些派生类(比方说涉及类型擦除的场景),我使用shared_ptr,因为我以上所述的原因。

不过,我在猜想是否有其他替代shared_ptr用于解决这个问题的方案或者说是否它们是在这种情况下的最佳解决方案。


2
static_pointer_cast 仅适用于类型为 std::shared_ptr<T> 的参数 - 它不能与 unique_ptr 一起使用。 - M.M
1
@M.M 是的,我知道,我在问题中几乎写了同样的话,说没有unique_ptr的等价物。 - skypjack
http://coliru.stacked-crooked.com/a/cb27300bea5d10a8 - 一个原型 - javaLover
1
@javaLover 嗯,我会说其他答案有更直接和清晰的方法,但是......它就是能用......就是这样。 :-) - skypjack
@javaLover,你的类型问题在于它只能执行一种类型转换(静态转换),而自由函数*_unique_cast模板可以为所有四种类型转换编写。 - Caleth
显示剩余9条评论
3个回答

55

#原始指针

解决您的问题的方法是获取原始(非拥有)指针并进行转换 - 然后只需让原始指针超出作用域,让其余的unique_ptr<Base>控制所拥有对象的生命周期。

像这样:

unique_ptr<Base> foo = fooFactory();

{
    Base* tempBase = foo.get();
    Derived* tempDerived = static_cast<Derived*>(tempBase);
} // tempBase and tempDerived go out of scope here, but foo remains -> no need to delete

#Unique_pointer_cast 另一个选项是使用unique_ptrrelease()函数将其包装成另一个unique_ptr。

像这样:

template<typename TO, typename FROM>
unique_ptr<TO> static_unique_pointer_cast (unique_ptr<FROM>&& old){
    return unique_ptr<TO>{static_cast<TO*>(old.release())};
    // conversion: unique_ptr<FROM>->FROM*->TO*->unique_ptr<TO>
}

unique_ptr<Base> foo = fooFactory();

unique_ptr<Derived> foo2 = static_unique_pointer_cast<Derived>(std::move(foo));

请记住,这会使旧指针foo失效。

#来自原始指针的参考 仅为回答完整性,该解决方案实际上是在评论中由 OP 提出的原始指针的微小修改。

与使用原始指针类似,可以将原始指针进行类型转换,然后通过取消引用来创建一个引用。 在这种情况下,重要的是确保所创建的引用的生命周期不会超过unique_ptr的生命周期。

示例:

unique_ptr<Base> foo = fooFactory();
Derived& bar = *(static_cast<Derived*>(foo.get()));
// do not use bar after foo goes out of scope

2
为什么不反转这些模板参数的顺序,让FROM被推导出来,就像在std::static_pointer_cast中一样? - aschepler
@Anedar 当然,我显然不想处理原始指针,但你还是给了我一个好的提示... static_unique_pointer_cast 的实现很有趣,但不幸的是它与存储这些指针的想法(在问题中表达)不太匹配,因为一旦它们被失效,它们必须在当前使用后重新初始化以供将来使用,所以这会导致在容器周围进行大量的工作。 - skypjack
2
@skypjack 所引发的问题是:指向对象的所有者是谁(即负责删除的人)?一次只有一个指针吗?->使用unique_ptr,可能一次只有一个。多个(可能是不同类的)?->使用shared pointers。对于不负责删除的指针,请使用原始指针。如果您决定只有一个所有者,则它们可能是获得指向同一对象的不同类的指针并保持所有权在原始对象上的唯一方法。 - Anedar
嗯,不是很容易。它是一组指向类型擦除类的指针,需要时进行前向转换,因此所有者只有一个,但我仍然可能遇到像它们是两个一样的问题。无论如何,关键是使用“get”获取原始指针,进行转换和解引用,以便返回引用,对其生命周期可以保证。否则,当然也可以尝试使用shared_ptr... :-) - skypjack
好的,这基本上是使用原始指针,但添加了一个解引用以避免之后处理指针逻辑的麻烦。听起来像是一个不错的解决方案,因为您说您可以保证引用的生命周期不会超过unique_ptr的生命周期。 - Anedar
@HowardHinnant 因为构造函数被标记为显式,我已经修复了它。请参见这里 - Anedar

7
我知道使用static_pointer_cast和unique_ptr会导致包含数据的共享所有权。如果你定义得当,显然的解决方案是转移所有权,这样源对象就变为空了。如果您不想转移所有权,那么只需使用原始指针即可。或者,如果您想要两个所有者,可以使用shared_ptr。似乎您的问题只在于实际转换操作,以及指针缺乏明确的所有权策略。如果您需要多个所有者,无论它们是否使用相同的类型,或者其中一个被转换为不同的类型,则不应使用unique_ptr。无论如何,这样做会导致两个unique_ptr同时存在,因此被禁止。这并不是它不存在的原因。它不存在是因为如果您需要它(并且只要给它唯一所有权的合理语义),则很容易自己编写。只需使用release()将指针取出进行转换,然后将其放入另一个unique_ptr中。简单而安全。对于shared_ptr来说情况并非如此,"显而易见"的解决方案并不能做正确的事情:
shared_ptr<Derived> p2(static_cast<Derived*>(p1.get());

那将创建两个不共享所有权(即它们都会尝试删除它,导致未定义行为)但拥有相同指针的shared_ptr对象。

当最初标准化shared_ptr时,没有安全的方法来做到这一点,因此定义了static_pointer_cast和相关的强制转换函数。它们需要访问shared_ptr管理信息的实现细节才能工作。

然而,在C++11标准化过程中,通过添加“别名构造函数”增强了shared_ptr,使您可以简单而安全地进行强制转换:

shared_ptr<Derived> p2(p1, static_cast<Derived*>(p1.get());

如果这个特性一直是shared_ptr的一部分,那么static_pointer_cast可能甚至不会被定义。

1
“它不存在是因为自己编写它非常简单”,这个观点也可以适用于STL中的其他一些部分(存在的部分),但我理解了你回答的重点。+1 - skypjack
你错过了句子结尾的部分:“如果需要的话”。如果某些东西难以或者不可能由用户正确编写(这在最初的shared_ptr转换中是真实的情况),或者如果它们很容易但每个人都在重新实现,那么将它们放入标准中是值得的。对于unique_ptr转换来说,两种情况都不成立,因为它很容易,但并不经常需要。 - Jonathan Wakely
1
我同意,老实说我觉得我正在面临一个XY问题,但是这个问题对我来说仍然很有趣,因为我想知道如果他们是我,其他人会怎么做。 - skypjack
3
std::exchange 很容易编写,但没有人使用它。那它是怎么进来的呢? :-) - Howard Hinnant
1
@HowardHinnant 我认为std::exchange非常像一只鸭子 - Barry

1
我想要在Anedar之前的回答中添加一些内容,该回答调用给定的std::unique_ptr< U >release()成员方法。如果想要实现dynamic_pointer_cast(除了static_pointer_cast)将std::unique_ptr< U >转换为std::unique_ptr< T >,则必须确保在dynamic_cast失败的情况下(即返回nullptr),由唯一指针保护的资源得到适当释放。否则,会发生内存泄漏。

代码

#include <iostream>
#include <memory>

template< typename T, typename U >
inline std::unique_ptr< T > dynamic_pointer_cast(std::unique_ptr< U > &&ptr) {
    U * const stored_ptr = ptr.release();
    T * const converted_stored_ptr = dynamic_cast< T * >(stored_ptr);
    if (converted_stored_ptr) {
        std::cout << "Cast did succeeded\n";
        return std::unique_ptr< T >(converted_stored_ptr);
    }
    else {
        std::cout << "Cast did not succeeded\n";
        ptr.reset(stored_ptr);
        return std::unique_ptr< T >();
    }
}

struct A { 
    virtual ~A() = default;
};
struct B : A {
    virtual ~B() { 
        std::cout << "B::~B\n"; 
    }
};
struct C : A {
    virtual ~C() { 
        std::cout << "C::~C\n"; 
    }
};
struct D { 
    virtual ~D() { 
        std::cout << "D::~D\n"; 
    }
};

int main() {

  std::unique_ptr< A > b(new B);
  std::unique_ptr< A > c(new C);
  std::unique_ptr< D > d(new D);

  std::unique_ptr< B > b1 = dynamic_pointer_cast< B, A >(std::move(b));
  std::unique_ptr< B > b2 = dynamic_pointer_cast< B, A >(std::move(c));
  std::unique_ptr< B > b3 = dynamic_pointer_cast< B, D >(std::move(d));
}

输出(可能顺序):
Cast did succeeded
Cast did not succeeded
Cast did not succeeded
B::~B
D::~D
C::~C

如果使用以下代码,则不会调用CD的析构函数:
template< typename T, typename U >
inline std::unique_ptr< T > dynamic_pointer_cast(std::unique_ptr< U > &&ptr) {
    return std::unique_ptr< T >(dynamic_cast< T * >(ptr.release()));
}

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