unique_ptr和shared_ptr的重载方法在多态性方面存在歧义。

26

在我的上一个问题获得提示后,我开始编写代码,但是遇到了重载 Scene::addObject 的问题。

为了概括相关部分并使其自包含,尽可能少地提供细节:

  • 我有一组从 Interface 继承的对象层级结构,其中有 FooBar;
  • 我有一个拥有这些对象的 Scene;
  • main 中的 Foo 应该是 unique_ptr,而 Bar 应该是 shared_ptr(出于上一个问题中解释的原因);
  • main 把它们传递给 Scene 实例来接管。

最小化的代码示例在此处

#include <memory>
#include <utility>

class Interface
{
public:
  virtual ~Interface() = 0;
};

inline Interface::~Interface() {}

class Foo : public Interface
{
};

class Bar : public Interface
{
};

class Scene
{
public:
  void addObject(std::unique_ptr<Interface> obj);
//  void addObject(std::shared_ptr<Interface> obj);
};

void Scene::addObject(std::unique_ptr<Interface> obj)
{
}

//void Scene::addObject(std::shared_ptr<Interface> obj)
//{
//}

int main(int argc, char** argv)
{
  auto scn = std::make_unique<Scene>();

  auto foo = std::make_unique<Foo>();
  scn->addObject(std::move(foo));

//  auto bar = std::make_shared<Bar>();
//  scn->addObject(bar);
}

去掉注释行的结果是:

error: call of overloaded 'addObject(std::remove_reference<std::unique_ptr<Foo, std::default_delete<Foo> >&>::type)' is ambiguous

   scn->addObject(std::move(foo));

                                ^

main.cpp:27:6: note: candidate: 'void Scene::addObject(std::unique_ptr<Interface>)'

 void Scene::addObject(std::unique_ptr<Interface> obj)

      ^~~~~

main.cpp:31:6: note: candidate: 'void Scene::addObject(std::shared_ptr<Interface>)'

 void Scene::addObject(std::shared_ptr<Interface> obj)

      ^~~~~
取消注释共享部分并注释掉独特的内容也可以编译,所以我认为问题就像编译器所说的那样,在于重载。然而,我需要这种重载,因为这两种类型都需要存储在某种集合中,并且它们确实被保留为指向基类的指针(可能全部移动到shared_ptr中)。
我通过按值传递两个参数来明确表明我在Scene中拥有它们(并增加了shared_ptr的引用计数)。对我来说不太清楚问题究竟出在哪里,我找不到任何其他示例。

std::unique_ptr<Foo> 转换为 std::shared_ptr<Scene> 和转换为 std::unique_ptr<Scene> 一样好。 - felix
2
不错的MCVE,不错的问题。我想看到更多这样的内容。 - YSC
1
对我来说,如果一个类接受两种不同类型的指向相同基类的指针,就是代码异味。这意味着该类内部的内存管理出现了问题。因此,要解决这个问题,我会删除其中一个方法。可能 unique_ptr 版本已经过时了,因为可以轻松地转换为 shared_ptr。另一种解决方法是不使用重载(至少重命名其中一个方法),这应该提高可读性。 - Marek R
5个回答

18
您遇到的问题是,shared_ptr (13) 的构造函数(不是显式的)与类似的“将派生类移动到基类”的unique_ptr (6) 构造函数一样匹配。请注意保留HTML标记。
template< class Y, class Deleter > 
shared_ptr( std::unique_ptr<Y,Deleter>&& r ); // (13)

构造一个shared_ptr,它管理当前由r管理的对象。与r相关联的删除器将被存储以便将来删除所管理的对象。调用后,r不再管理任何对象。
如果std::unique_ptr::pointer与T*不兼容,则此重载不参与重载解析。如果r.get()是空指针,则此重载等同于默认构造函数(1)。 (自C++17起)
template< class U, class E >
unique_ptr( unique_ptr<U, E>&& u ) noexcept; //(6)

6) 构造一个unique_ptr,将所有权从u转移给*this,其中u使用指定的删除器(E)构造。

仅当以下所有条件均为true时,此构造函数才参与重载分辨:

a) unique_ptr<U, E>::pointer可以隐式转换为指针

b) U不是数组类型

c) 要么Deleter是引用类型且ED相同,要么Deleter不是引用类型且E可以隐式转换为D

在非多态情况下,您正在使用非模板移动构造函数从unique_ptr<T>&&构造unique_ptr<T>。在这种情况下,重载分辨会优先选择非模板。


我假设Scene存储shared_ptr<Interface>。在这种情况下,您不需要为unique_ptr重载addObject,您只需允许调用中的隐式转换即可。


为什么没有多态性也不会产生歧义? - snake_style
@snake_style 澄清了。移动构造函数是一个误称。 - Caleth
我指的是结构体D { }; void func(shared_ptr<D> ptr){} void func(unique_ptr<D> ptr){}int main() { auto p = make_unique<D>(); func(std::move(p)); } 为什么这个示例中这些构造函数没有被调用? - snake_style
在这种情况下,您没有为unique_ptr选择模板构造函数,而是非模板移动构造函数。重载解析更倾向于非模板转换。 - Caleth

5
另一个答案已经解释了歧义和可能的解决方案。如果您最终需要这两种重载,还有另一种方法:在这种情况下,您始终可以添加另一个参数来打破歧义并使用标签调度。样板代码隐藏在Scene的私有部分中。
class Scene
{
    struct unique_tag {};
    struct shared_tag {};
    template<typename T> struct tag_trait;
    // Partial specializations are allowed in class scope!
    template<typename T, typename D> struct tag_trait<std::unique_ptr<T,D>> { using tag = unique_tag; };
    template<typename T>             struct tag_trait<std::shared_ptr<T>>   { using tag = shared_tag; };

  void addObject_internal(std::unique_ptr<Interface> obj, unique_tag);
  void addObject_internal(std::shared_ptr<Interface> obj, shared_tag);

public:
    template<typename T>
    void addObject(T&& obj)
    {
        addObject_internal(std::forward<T>(obj),
            typename tag_trait<std::remove_reference_t<T>>::tag{});
    }
};

完整的可编译示例在这里


3
你声明了两个重载函数,一个接受 std::unique_ptr<Interface> ,另一个接受 std::shared_ptr<Interface> ,但你传入的参数类型是 std::unique_ptr<Foo>。由于没有直接匹配的函数,编译器必须执行一次转换才能调用该函数。

有一种可用的转换到 std::unique_ptr<Interface> (将简单类型转换为指向基类的唯一指针),以及另一种到 std::shared_ptr<Interface> (转换为指向基类的共享指针)。这两种转换具有相同的优先级,因此编译器无法确定使用哪种转换,所以函数是模糊的。

如果传递 std::unique_ptr<Interface>std::shared_ptr<Interface>,则不需要进行转换,因此也就不存在歧义。

解决方法很简单,只需移除 unique_ptr 重载并始终转换为 shared_ptr。这假定两个重载函数行为相同,如果它们不同,重命名其中一个方法可能更合适。


1
jrok提供的解决方案已经非常不错了。以下方法可以更好地重复使用代码:
#include <memory>
#include <utility>
#include <iostream>
#include <type_traits>

namespace internal {
    template <typename S, typename T>
    struct smart_ptr_rebind_trait {};

    template <typename S, typename T, typename D>
    struct smart_ptr_rebind_trait<S,std::unique_ptr<T,D>> { using rebind_t = std::unique_ptr<S>; };

    template <typename S, typename T>
    struct smart_ptr_rebind_trait<S,std::shared_ptr<T>> { using rebind_t = std::shared_ptr<S>; };

}

template <typename S, typename T>
using rebind_smart_ptr_t = typename internal::smart_ptr_rebind_trait<S,std::remove_reference_t<T>>::rebind_t;

class Interface
{
public:
  virtual ~Interface() = 0;
};

inline Interface::~Interface() {}

class Foo : public Interface {};

class Bar : public Interface {};

class Scene
{
  void addObject_internal(std::unique_ptr<Interface> obj) { std::cout << "unique\n"; }

  void addObject_internal(std::shared_ptr<Interface> obj) { std::cout << "shared\n"; }

public:

  template<typename T>
  void addObject(T&& obj) {
    using S = rebind_smart_ptr_t<Interface,T>;
    addObject_internal( S(std::forward<T>(obj)) );
  }   
};

int main(int argc, char** argv)
{
  auto scn = std::make_unique<Scene>();

  auto foo = std::make_unique<Foo>();
  scn->addObject(std::move(foo));

  auto bar = std::make_shared<Bar>();
  scn->addObject(bar); // ok
}

我们在这里做的是首先介绍一些帮助类,允许重新绑定智能指针。

0
在我的主函数中,Foos 应该是 unique_ptrs,而 Bars 应该是 shared_ptrs(原因在前一个问题中解释);
你能否以指向 Foo 和指向 Bar 的指针为重载条件,而不是以指向 Interface 的指针为重载条件,因为你想要对它们进行不同的处理?

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