web-dev-qa-db-ja.com

unique_ptrおよびshared_ptrのオーバーロードメソッドは多態性とあいまいです

前の質問 の回答からヒントを得た後、コードをコーディングすると、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)

      ^~~~~

共有のコメントを外し、ユニークなものをコメントすることもコンパイルするので、コンパイラーが言うように、問題はオーバーロードにあると思います。ただし、これらの型はいずれも何らかのコレクションに格納する必要があるため、オーバーロードが必要であり、実際にベースへのポインターとして保持されます(おそらくすべてがshared_ptrsに移動されます)。

Sceneの所有権を取得している(およびshared_ptrsの参照カウンターを増やしている)ことを明確にしたいので、両方の値を渡します。問題がどこにあるのか私には本当にはっきりしていません。他の場所でこの例を見つけることができませんでした。

25

あなたが遭遇している問題は、このコンストラクタ _shared_ptr_(13) (明示的ではありません)のコンストラクタです。 _unique_ptr_(6) (明示的でもありません)。

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

13)rによって現在管理されているオブジェクトを管理する_shared_ptr_を構築します。 rに関連付けられた削除者は、管理対象オブジェクトの将来の削除のために保存されます。 rは、呼び出し後にオブジェクトを管理しません。

_std::unique_ptr<Y, Deleter>::pointer_が_T*_と互換性がない場合、このオーバーロードはオーバーロード解決に関与しません。 r.get()がNULLポインターの場合、このオーバーロードはデフォルトのコンストラクター(1)と同等です。 (C++ 17以降)

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

6)所有権をuから_unique_ptr_に転送することで_*this_を構築します。ここで、uは指定された削除者(E)で構築されます。

このコンストラクタは、次のすべてに該当する場合にのみオーバーロード解決に関与します。

a)_unique_ptr<U, E>::pointer_は暗黙的にポインターに変換可能

b)Uは配列型ではありません

c)Deleterが参照型でEDと同じ型であるか、Deleterが参照型ではなくEDに暗黙的に変換可能

非ポリモーフィックの場合、非テンプレート移動コンストラクターを使用する_unique_ptr<T>_から_unique_ptr<T>&&_を構築しています。オーバーロード解決は非テンプレートを優先します


Sceneは_shared_ptr<Interface>_ sを格納すると仮定します。その場合、_unique_ptr_に対してaddObjectをオーバーロードする必要はありません。呼び出しで暗黙的な変換を許可することができます。

18
Caleth

もう1つの答えは、あいまいさと可能な解決策を説明しています。両方のオーバーロードが必要になった場合の別の方法を次に示します。このような場合はいつでも別のパラメーターを追加して、あいまいさを解消し、タグディスパッチを使用できます。ボイラープレートコードは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。 です

4
jrok

2つのオーバーロードを宣言しました。1つはstd::unique_ptr<Interface>を受け取り、もう1つはstd::shared_ptr<Interface>を受け取りますが、std::unique_ptr<Foo>型のパラメーターを渡します。どの関数も直接一致しないため、コンパイラは関数を呼び出すために変換を実行する必要があります。

std::unique_ptr<Interface>(基本クラスへの一意のポインターへの単純な型変換)とstd::shared_ptr<Interface>(基本クラスへの共有ポインターへの変更)に使用できる1つの変換があります。これらの変換は同じ優先度を持っているため、使用する変換がコンパイラーにわからないため、関数があいまいになります。

std::unique_ptr<Interface>またはstd::shared_ptr<Interface>を渡す場合、変換は必要ないため、あいまいさはありません。

解決策は、単にunique_ptrオーバーロードを削除し、常にshared_ptrに変換することです。これは、2つのオーバーロードが同じ振る舞いをしていると仮定しています。メソッドの名前を変更しない方が適切な場合があります。

3
Alan Birtles

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
}

ここで行うことは、最初にスマートポインターを再バインドできるヘルパークラスをいくつか導入することです。

1
Handy999

メインのFoosはunique_ptrs、Barsはshared_ptrsです(前の質問で説明した理由により)。

Fooへのポインターの代わりにBarへのポインターと_Interfaceへのポインターの観点でオーバーロードできますか?

0
WaffleSouffle