独占指针的动态转换

56

C++11提供了一些用于强制转换shared_ptr的函数,就像在Boost中一样:

std::static_pointer_cast
std::dynamic_pointer_cast
std::const_pointer_cast

然而,我想知道为什么没有类似于unique_ptr的等效函数。

考虑以下简单的示例:

class A { virtual ~A(); ... }
class B : public A { ... }

unique_ptr<A> pA(new B(...));

unique_ptr<A> qA = std::move(pA); // This is legal since there is no casting
unique_ptr<B> pB = std::move(pA); // This is not legal

// I would like to do something like:
// (Of course, it is not valid, but that would be the idea)
unique_ptr<B> pB = std::move(std::dynamic_pointer_cast<B>(pA));

这种使用模式为什么被反对,因此,unique_ptr中没有提供与shared_ptr中存在的等效函数?


4
如果动态转换失败,您是否希望之前拥有的对象被销毁? - CB Bailey
2
如果由pA指向的对象不能转换为类型B(即dynamic_cast<B>(pA.get())失败),那么您希望对该对象采取什么操作?pA应该保持所有权吗?还是应该被销毁? - cdhowie
2
@CharlesBailey 这实际上是一个很好的观点。这实际上是一个重要的实现决策。如果 dynamic_cast 失败,"常识" 会建议中止转换,而不修改原始指针。这实际上是 cdhowie 答案中的行为。 - betabandido
9个回答

41
除了Mark Ransom的答案之外,unique_ptr<X, D>可能甚至不存储一个X*
如果删除器定义了类型D::pointer,那么存储的就是它,而它可能不是一个真正的指针,只需要满足NullablePointer要求,并且(如果调用unique_ptr<X,D>::get())有一个返回X&operator*,但它不需要支持转换为其他类型。 unique_ptr非常灵活,不一定表现得像内置指针类型。
如请求所示,这里是一个示例,其中存储的类型不是指针,因此不可能进行转换。它有点牵强附会,但它将一个虚构的数据库API(定义为C风格的API)包装在C++ RAII样式的API中。 OpaqueDbHandle类型符合NullablePointer要求,但只存储一个整数,该整数用作查找实际DB连接的键通过某种实现定义的映射。 我展示这个示例并不是为了展示出伟大的设计,而只是为了演示使用unique_ptr来管理一个非可复制、可移动的资源,它不是一个动态分配的指针,其中“删除器”在unique_ptr超出范围时不仅调用析构函数并释放内存。
#include <memory>

// native database API
extern "C"
{
  struct Db;
  int db_query(Db*, const char*);
  Db* db_connect();
  void db_disconnect(Db*);
}

// wrapper API
class OpaqueDbHandle
{
public:
  explicit OpaqueDbHandle(int id) : id(id) { }

  OpaqueDbHandle(std::nullptr_t) { }
  OpaqueDbHandle() = default;
  OpaqueDbHandle(const OpaqueDbHandle&) = default;

  OpaqueDbHandle& operator=(const OpaqueDbHandle&) = default;
  OpaqueDbHandle& operator=(std::nullptr_t) { id = -1; return *this; }

  Db& operator*() const;

  explicit operator bool() const { return id > 0; }

  friend bool operator==(const OpaqueDbHandle& l, const OpaqueDbHandle& r)
  { return l.id == r.id; }

private:
  friend class DbDeleter;
  int id = -1;
};

inline bool operator!=(const OpaqueDbHandle& l, const OpaqueDbHandle& r)
{ return !(l == r); }

struct DbDeleter
{
  typedef OpaqueDbHandle pointer;

  void operator()(pointer p) const;
};

typedef std::unique_ptr<Db, DbDeleter> safe_db_handle;

safe_db_handle safe_connect();

int main()
{
  auto db_handle = safe_connect();
  (void) db_query(&*db_handle, "SHOW TABLES");
}


// defined in some shared library

namespace {
  std::map<int, Db*> connections;      // all active DB connections
  std::list<int> unused_connections;   // currently unused ones
  int next_id = 0;
  const unsigned cache_unused_threshold = 10;
}

Db& OpaqueDbHandle::operator*() const
{
   return connections[id];
}

safe_db_handle safe_connect()
{
  int id;
  if (!unused_connections.empty())
  {
    id = unused_connections.back();
    unused_connections.pop_back();
  }
  else
  {
    id = next_id++;
    connections[id] = db_connect();
  }
  return safe_db_handle( OpaqueDbHandle(id) );
}

void DbDeleter::operator()(DbDeleter::pointer p) const
{
  if (unused_connections.size() >= cache_unused_threshold)
  {
    db_disconnect(&*p);
    connections.erase(p.id);
  }
  else
    unused_connections.push_back(p.id);
}

谢谢你的回答。看起来比我最初想象的要复杂得多。你能举个例子,展示如何构建一个不存储指针的 unique_ptr 吗? - betabandido
4
+1 我从来没有想过使用 unique_ptr 来实现 RAII(资源获取即初始化)模式处理数据库 :) 谢谢你提供的例子。 - betabandido

34

您所提到的函数每次都会制作指针的副本。由于您无法制作unique_ptr副本,因此为其提供这些函数没有意义。


18
没错,但如果唯一的意图是移动指针呢?在这种情况下,unique_ptr 的转换函数不会进行复制,只会移动(或转移)指针。 - betabandido
1
@betabandido:如果动态转换失败怎么办? - Puppy
我最终决定接受这个答案,因为即使可能性存在,但在尝试向下转换 unique_ptr 时似乎存在大量问题。 - betabandido
@betabandido,只有在使用 dynamic_cast 时才会出现问题... 使用 static_cast 应该没有问题,但是目前没有相应的函数。 - IceFire

14

在Dave的答案基础上,这个模板函数将尝试将一个unique_ptr的内容移动到另一个不同类型的指针中。

  • 如果返回true,则:
    • 源指针为空。目标指针将被清除以符合"将此指针的内容(什么也没有)移动到那个指针"的语义要求。
    • 源指针所指向的对象可以转换为目标指针类型。源指针将被清空,并且目标指针将指向它原来指向的相同对象。目标指针将获得源指针的删除器(仅在使用第一个重载时)。
  • 如果返回false,则操作失败。两个指针状态都不会改变。

 

template <typename T_SRC, typename T_DEST, typename T_DELETER>
bool dynamic_pointer_move(std::unique_ptr<T_DEST, T_DELETER> & dest,
                          std::unique_ptr<T_SRC, T_DELETER> & src) {
    if (!src) {
        dest.reset();
        return true;
    }

    T_DEST * dest_ptr = dynamic_cast<T_DEST *>(src.get());
    if (!dest_ptr)
        return false;

    std::unique_ptr<T_DEST, T_DELETER> dest_temp(
        dest_ptr,
        std::move(src.get_deleter()));

    src.release();
    dest.swap(dest_temp);
    return true;
}

template <typename T_SRC, typename T_DEST>
bool dynamic_pointer_move(std::unique_ptr<T_DEST> & dest,
                          std::unique_ptr<T_SRC> & src) {
    if (!src) {
        dest.reset();
        return true;
    }

    T_DEST * dest_ptr = dynamic_cast<T_DEST *>(src.get());
    if (!dest_ptr)
        return false;

    src.release();
    dest.reset(dest_ptr);
    return true;
}

请注意,第二个重载版本对于声明为std::unique_ptr<A>std::unique_ptr<B>的指针是必需的。第一个函数将无法工作,因为第一个指针实际上将是类型为std::unique_ptr<A, default_delete<A> >的指针,而第二个指针是类型std::unique_ptr<A, default_delete<B> >;删除器类型不兼容,所以编译器不允许您使用此函数。


因此,考虑到可能存在一个实现,您能想到为什么动态转换unique_ptr可能会是不好的实践吗?我同意在unique_ptr上使用move可能不是良好编码的示例,但在某些情况下这样做实际上可能会被证明是有用的。 - betabandido
我不能说它比动态转换其他指针更糟糕。只是当你处理独占所有权指针时,你必须解决特殊的“如果转换失败会发生什么”问题(正如我在这些函数中所做的那样)。 - cdhowie
可能需要查看一些类似的代码:https://dev59.com/U18d5IYBdhLWcg3w1VI1#26377517 - user2746401
@user2746401,感谢提供链接。我编辑了我的答案,使用std::move()来传递删除器,就像那个答案一样。 - cdhowie

7

这并不是对于“为什么”的回答,但是却是一种实现的方法...

std::unique_ptr<A> x(new B);
std::unique_ptr<B> y(dynamic_cast<B*>(x.get()));
if(y)
    x.release();

这并不完全干净,因为在短暂的时间里,有两个unique_ptr认为它们拥有同一个对象。正如评论中所指出的,如果您使用自定义删除器,则还需要管理移动它(但这非常罕见)。


6
如果你有一个 unique_ptr<A, Deleter>,情况会更加复杂,因为你需要移动 deleter。 - Jonathan Wakely
虽然在临时情况下拥有两个实例没有问题:std::unique_ptr<A> x(new B); const auto yp = dynamic_cast<B*>(x.get()); std::unique_ptr<B> y(yp != nullptr ? (x.release(), yp) : nullptr); - Arne Vogel

4

如果您只在小范围内使用下行转换指针,一种替代方案是将unique_ptr管理的对象的引用简单地进行下行转换:

auto derived = dynamic_cast<Derived&>(*pBase);
derived.foo();

4
这里有一个 C++11 的方法,您觉得如何:

template <class T_SRC, class T_DEST>
inline std::unique_ptr<T_DEST> unique_cast(std::unique_ptr<T_SRC> &&src)
{
    if (!src) return std::unique_ptr<T_DEST>();

    // Throws a std::bad_cast() if this doesn't work out
    T_DEST *dest_ptr = &dynamic_cast<T_DEST &>(*src.get());

    src.release();
    return std::unique_ptr<T_DEST>(dest_ptr);
}

1
我喜欢它在转换失败时抛出异常(对于某些用例)。但我认为当源是空时仍然返回一个空指针/空对象是不好的。这会导致API不一致,您仍然会在某些无法转换的情况下返回NULL,但在其他情况下不返回。因此,也许可以这样做:如果(!src)throw std :: bad_cast()? - Giel
谢谢,代码片段很不错。我已发布了一个优化版本。 - jaba

1

我修改了@Bob F的答案https://dev59.com/2mgu5IYBdhLWcg3w4665#14777419,现在您只需像其他类型的转换一样使用一个模板参数即可。

template <class destinationT, typename sourceT>
std::unique_ptr<destinationT> unique_cast(std::unique_ptr<sourceT>&& source)
{
    if (!source)
        return std::unique_ptr<destinationT>();

    // Throws a std::bad_cast() if this doesn't work out
    destinationT* dest_ptr = &dynamic_cast<destinationT&>(*source.get());

    source.release();
    return std::unique_ptr<destinationT>(dest_ptr);
}

更新(非抛出版本):
template <class destinationT, typename sourceT>
std::unique_ptr<destinationT> unique_cast(std::unique_ptr<sourceT>&& source)
{
    if (!source)
        return std::unique_ptr<destinationT>();

    destinationT* dest_ptr = dynamic_cast<destinationT*>(source.get());
    if(dest_ptr)
        source.release();
    
    return std::unique_ptr<destinationT>(dest_ptr);
}

使用方法:

std::unique_ptr<MyClass> obj = unique_cast<MyClass>(std::make_unique<MyOtherClass>()); 

如果dynamic_cast失败,这会导致内存泄漏。在检查转换是否成功后,您必须才能调用release。请参见https://dev59.com/U18d5IYBdhLWcg3w1VI1#26377517。 - Morty
1
谢谢指出。但是因为有一个 dynamic_cast 到一个引用,所以会抛出 std::bad_cast 异常,而 release() 永远不会被执行。调用代码必须处理异常。我已经添加了一个非抛出版本。 - jaba
我的错!我在转换中漏掉了两个“&”符号。 - Morty

0

我喜欢cdhowie的答案...但我希望他们使用返回值而不是使用out-args。这是我想出来的:

template <typename T_DEST, typename T_SRC, typename T_DELETER>
std::unique_ptr<T_DEST, T_DELETER>
dynamic_pointer_cast(std::unique_ptr<T_SRC, T_DELETER> & src)
{
  if (!src)
    return std::unique_ptr<T_DEST, T_DELETER>(nullptr);

  T_DEST * dest_ptr = dynamic_cast<T_DEST *>(src.get());
  if (!dest_ptr)
    return std::unique_ptr<T_DEST, T_DELETER>(nullptr);

  std::unique_ptr<T_DEST, T_DELETER> dest_temp(dest_ptr, std::move(src.get_deleter()));

  src.release();

  return dest_temp;
}

template <typename T_SRC, typename T_DEST>
std::unique_ptr<T_DEST>
dynamic_pointer_cast(std::unique_ptr<T_SRC> & src)
{
  if (!src)
    return std::unique_ptr<T_DEST>(nullptr);

  T_DEST * dest_ptr = dynamic_cast<T_DEST *>(src.get());
  if (!dest_ptr)
    return std::unique_ptr<T_DEST>(nullptr);

  std::unique_ptr<T_DEST> dest_temp(dest_ptr);

  src.release();

  return dest_temp;
}

我将其放入GitHub存储库中: https://github.com/friedmud/unique_ptr_cast


0

根据我目前看到的所有示例,我得出了这个版本。

template <typename T_DEST, typename T_SRC, typename T_DELETER>
std::unique_ptr<T_DEST, T_DELETER>
dynamic_pointer_cast(std::unique_ptr<T_SRC, T_DELETER>&& src)
{
    // When nullptr, just return nullptr
    if (!src) return std::unique_ptr<T_DEST, T_DELETER>(nullptr);

    // Perform dynamic_cast, throws std::bad_cast() if this doesn't work out
    T_DEST* dest_ptr = dynamic_cast<T_DEST*>(src.get());
    
    // Do not return nullptr on bad_cast
    //if (!dest_ptr) return std::unique_ptr<T_DEST, T_DELETER>(nullptr);
    
    // But throw std::bad_cast instead
    if (!dest_ptr) throw std::bad_cast();

    // Move into new unique_ptr
    std::unique_ptr<T_DEST, T_DELETER> dest_temp(dest_ptr, std::move(src.get_deleter()));
    src.release();

    return dest_temp;
}

template <typename T_DEST, typename T_SRC>
std::unique_ptr<T_DEST>
dynamic_pointer_cast(std::unique_ptr<T_SRC>&& src)
{
    // When nullptr, just return nullptr
    if (!src) return std::unique_ptr<T_DEST>(nullptr);

    // Perform dynamic_cast, throws std::bad_cast() if this doesn't work out
    T_DEST* dest_ptr = dynamic_cast<T_DEST*>(src.get());
    
    // Do not return nullptr on bad_cast
    //if (!dest_ptr) return std::unique_ptr<T_DEST>(nullptr);
    
    // But throw std::bad_cast instead
    if (!dest_ptr) throw std::bad_cast();

    // Move into new unique_ptr
    std::unique_ptr<T_DEST> dest_temp(dest_ptr);
    src.release();

    return dest_temp;
}

如果你想让 dynamic_cast 抛出 std::bad_cast 异常,那么只需要将其转换成引用即可

像这样使用

auto src = std::make_unique<Base>();
auto dst = dynamic_pointer_cast<Derived>(std::move(src));
auto dst2 = dynamic_pointer_cast<Derived>(FunctionReturningBase());

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