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