【问题标题】:Overload method for unique_ptr and shared_ptr is ambiguous with polymorphismunique_ptr 和 shared_ptr 的重载方法与多态性不明确
【发布时间】:2019-04-28 01:17:34
【问题描述】:

previous question 的回答中得到提示后,我遇到了重载 Scene::addObject 的问题。

重申相关部分并使其自成一体,尽可能减少细节:

  • 我有一个继承自Interface 的对象层次结构,其中有Foos 和Bars;
  • 我有一个拥有这些对象的Scene
  • Foos 将是 unique_ptrs 和 Bars 将是 shared_ptrs 在我的主要(原因在上一个问题中解释);
  • main 将它们传递给 Scene 实例,该实例取得所有权。

最小代码示例是this:

#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)

      ^~~~~

取消注释共享和注释独特的东西也可以编译,所以我认为问题在于,正如编译器所说,在重载中。但是我需要重载,因为这两种类型都需要存储在某种集合中,并且它们确实作为指向 base 的指针保存(可能全部移入 shared_ptrs)。

我将按值传递这两个值,因为我想明确说明我拥有Scene 的所有权(并增加shared_ptrs 的引用计数器)。我不太清楚问题出在哪里,我在其他地方找不到任何这样的例子。

【问题讨论】:

  • std::unique_ptr&lt;Foo&gt; 转换为std::shared_ptr&lt;Scene&gt; 与转换为std::unique_ptr&lt;Scene&gt; 一样好。
  • 很好的 MCVE,很好的问题。我希望看到更多。
  • 对我来说,如果类为同一个基类接受两种不同类型的指针,这是一种代码味道。这意味着该类中的内存管理发生了一些不好的事情。所以要修复它,我会删除其中一种方法。可能unique_ptr 版本已过时,因为可以轻松转换为shared_ptr。解决此问题的其他方法是不要使用重载(重命名至少一种方法)应该提高可读性。

标签: c++ c++14 overloading shared-ptr unique-ptr


【解决方案1】:

您遇到的问题是shared_ptr (13) 的这个构造函数(不是显式的),与unique_ptr (6) 的类似“移动派生到基”构造函数(也不是显式的)一样匹配。

template< class Y, class Deleter > 
shared_ptr( std::unique_ptr<Y,Deleter>&& r ); // (13)

13) 构造一个shared_ptr,它管理当前由r 管理的对象。存储与r 关联的删除器以供将来删除托管对象。 r 调用后不管理任何对象。

如果std::unique_ptr&lt;Y, Deleter&gt;::pointerT* 不兼容,则此重载不参与重载解析。如果r.get() 是空指针,则此重载等效于默认构造函数 (1)。 (C++17 起)

template< class U, class E >
unique_ptr( unique_ptr<U, E>&& u ) noexcept; //(6)

6) 通过将所有权从u 转移到*this 来构造unique_ptr,其中u 是使用指定的删除器(E) 构造的。

仅当以下所有条件都为真时,此构造函数才参与重载决议:

a) unique_ptr&lt;U, E&gt;::pointer 可以隐式转换为指针

b) U 不是数组类型

c) Deleter 是引用类型,E 是与 D 相同的类型,或者 Deleter 不是引用类型,E 可以隐式转换为 D

在非多态情况下,您正在从unique_ptr&lt;T&gt;&amp;&amp; 构造一个unique_ptr&lt;T&gt;,它使用非模板移动构造函数。那里的重载决议更喜欢非模板


我将假设Scene 存储shared_ptr&lt;Interface&gt;s。在这种情况下,您不需要为unique_ptr 重载addObject,您可以只允许调用中的隐式转换。

【讨论】:

  • 为什么没有多态就没有歧义?
  • @snake_style 已澄清。移动构造函数用词不当
  • 我的意思是 struct D { }; void func(shared_ptr ptr){} void func(unique_ptr ptr){} int main() { auto p = make_unique();函数(标准::移动(p));为什么这些 ctor 在这个示例中没有在这里调用?
  • @snake_style 在这种情况下,您没有为unique_ptr 选择模板构造函数,而是选择了非模板移动构造函数。重载解析更喜欢非模板转换
【解决方案2】:

另一个答案解释了歧义和可能的解决方案。这是另一种方法,以防您最终需要两个重载;在这种情况下,您总是可以添加另一个参数来打破歧义并使用标签调度。样板代码隐藏在 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{});
    }
};

完整的可编译示例是here.

【讨论】:

    【解决方案3】:

    您已经声明了两个重载,一个采用std::unique_ptr&lt;Interface&gt;,一个采用std::shared_ptr&lt;Interface&gt;,但传入的是std::unique_ptr&lt;Foo&gt; 类型的参数。由于您的函数都没有直接匹配,因此编译器必须执行转换才能调用您的函数。

    std::unique_ptr&lt;Interface&gt;(简单类型转换为指向基类的唯一指针)和std::shared_ptr&lt;Interface&gt;(更改为指向基类的共享指针)可以进行一种转换。这些转换具有相同的优先级,因此编译器不知道要使用哪种转换,因此您的函数不明确。

    如果您通过std::unique_ptr&lt;Interface&gt;std::shared_ptr&lt;Interface&gt;,则不需要转换,因此不会有歧义。

    解决方案是简单地删除unique_ptr 重载并始终转换为shared_ptr。这假设两个重载具有相同的行为,如果它们不重命名其中一个方法可能更合适。

    【讨论】:

      【解决方案4】:

      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
      }
      

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

      【讨论】:

        【解决方案5】:

        Foos 是 unique_ptrs 和 Bars 是 shared_ptrs 在我的主要(原因在上一个问题中解释);

        您能否重载指向Foo 和指向Bar 的指针而不是指向Interface 的指针,因为您想区别对待它们?

        【讨论】:

          猜你喜欢
          • 2011-06-25
          • 2011-06-22
          • 1970-01-01
          • 2017-06-06
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-10-15
          相关资源
          最近更新 更多