Метод перегрузки для unique_ptr и shared_ptr неоднозначен с полиморфизмом - программирование
Подтвердить что ты не робот

Метод перегрузки для unique_ptr и shared_ptr неоднозначен с полиморфизмом

Кодируя вещи после получения подсказки из моего предыдущего ответа на вопрос, я столкнулся с проблемой перегрузки Scene :: addObject.

Чтобы повторить соответствующие биты и сделать их самодостаточными, с наименьшим количеством возможных деталей:

  • У меня есть иерархия объектов, наследующих от Interface из которых есть Foo и Bar;
  • У меня есть Scene которая владеет этими объектами;
  • Foo должен быть unique_ptr а Bar должен быть shared_ptr в моем main (по причинам, объясненным в предыдущем вопросе);
  • 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). Мне не совсем понятно, где проблема вообще, и я не смог найти ни одного примера в другом месте.

4b9b3361

Ответ 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<Y, Deleter>::pointer не совместим с T*. Если r.get() является нулевым указателем, эта перегрузка эквивалентна конструктору по умолчанию (1). (начиная с С++ 17)

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

6) Создает unique_ptr, передавая владение от u к *this, где u создается с указанным удалителем (E).

Этот конструктор участвует в разрешении перегрузки только в том случае, если выполняется все следующее:

a) unique_ptr<U, E>::pointer неявно преобразуется в указатель

б) U не является типом массива

c) Либо Deleter является ссылочным типом, а E является тем же типом, что и D, либо Deleter не является ссылочным типом, и E неявно преобразуется в D

В неполиморфном случае вы создаете unique_ptr<T> из unique_ptr<T>&&, который использует не шаблонный конструктор перемещения. Там разрешение перегрузки предпочитает не шаблон


Я собираюсь предположить, что Scene хранит shared_ptr<Interface> s. В этом случае вам не нужно перегружать addObject для 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{});
    }
};

Полный скомпилированный пример здесь.

Ответ 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. Это предполагает, что две перегрузки имеют одинаковое поведение, если они не переименовывают, один из методов может быть более подходящим.

Ответ 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 в моем main (по причинам, объясненным в предыдущем вопросе);

Вы можете перегрузить в терминах указателя-to - Foo и указатель-to - Bar вместо указателя-to - Interface, так как вы хотите, чтобы относиться к ним по- разному?