C++中std::unique_ptr无法在map中编译

7

我目前正尝试将一个std::unique_ptr存储在std::unordered_map中,但是我得到了一个奇怪的编译错误。 相关代码:

#pragma once

#include "Entity.h"

#include <map>
#include <memory>

class EntityManager {
private:
    typedef std::unique_ptr<Entity> EntityPtr;
    typedef std::map<int, EntityPtr> EntityMap;

    EntityMap map;
public:

    /*
    Adds an Entity
    */
    void addEntity(EntityPtr);

    /*
    Removes an Entity by its ID
    */
    void removeEntity(int id) {
        map.erase(id);
    }

    Entity& getById(int id) {
        return *map[id];
    }
};

void EntityManager::addEntity(EntityPtr entity) {
    if (!entity.get()) {
        return;
    }

    map.insert(EntityMap::value_type(entity->getId(), std::move(entity)));
}

这是编译错误:

c:\program files (x86)\microsoft visual studio 12.0\vc\include\tuple(438): error C2280: 'std::unique_ptr<Entity,std::default_delete<_Ty>>::unique_ptr(const std::unique_ptr<_Ty,std::default_delete<_Ty>> &)' : attempting to reference a deleted function
1>          with
1>          [
1>              _Ty=Entity
1>          ]
1>          c:\program files (x86)\microsoft visual studio 12.0\vc\include\memory(1486) : see declaration of 'std::unique_ptr<Entity,std::default_delete<_Ty>>::unique_ptr'
1>          with
1>          [
1>              _Ty=Entity
1>          ]
1>          This diagnostic occurred in the compiler generated function 'std::pair<const _Kty,_Ty>::pair(const std::pair<const _Kty,_Ty> &)'
1>          with
1>          [
1>              _Kty=int
1>  ,            _Ty=EntityManager::EntityPtr
1>          ]

使用clang++ 3.5编译没有问题。 - Ryan Haining
3
关于编译时错误我不清楚,但是这个调用:map.insert(EntityMap::value_type(entity->getId(), std::move(entity)))本身就不安全,因为无法确定将entity移动到函数参数中是在entity->getId()求值之前还是之后。为了保险起见,应该将getId()赋值给一个临时变量并将其传递给函数。 - Michael Burr
1
иҝҷеҫҲеҸҜиғҪжҳҜMSVCйҷ„еёҰзҡ„stdеә“зҡ„й—®йўҳгҖӮеҜ»жүҫй”ҷиҜҜжҠҘе‘ҠпјҢжҲ–иҖ…и®ҫзҪ®дёҖдёӘhttp://ssccr.org并жҸҗдәӨдёҖдёӘпјҹ - Yakk - Adam Nevraumont
1
你可以尝试使用以下代码:map.insert(std::move(std::make_pair(entity->getId(), std::move(entity)))); 或者尝试使用 std::forward。我之前也遇到过这个错误。我不记得我在VS2012中是如何解决它的,但我确定它涉及到了一个 forward 或 move。另一个选项是将 nullptr 插入到 map 中,然后使用索引运算符对其进行移动赋值。 - Brandon
1
它通过右值引用传递,这将把所有权从临时对象转移给map。这就是移动构造函数的作用。例如:std::unique_ptr<int> up(new int); std::unique_ptr<int> up2(std::move(up)); 将int指针的所有权从 up 转移到了 up2 - Ryan Haining
显示剩余16条评论
4个回答

7
错误是因为在代码的某个地方,map希望复制一个std::pair<int, std::unique_ptr<Entity>>,然而没有能够实现此操作的复制构造函数,因为unique_ptr不可复制。这是为了防止多个指针拥有相同的内存而明确不可能的。
因此,在std::move之前,没有办法使用不可复制的元素。
这里有一些解决方案 然而,在c++11中,Map可以利用std::move来处理不可复制的值。
这是通过提供另一个插入运算符来完成的,该运算符重载以包括此签名:
template< class P > std::pair<iterator,bool> insert( P&& value );

这意味着一个能够转换为value_type的类的右值可以被用作参数。旧的insert仍然可用:
std::pair<iterator,bool> insert( const value_type& value );

这个插入实际上是复制了一个value_type的值,但由于value_type不可复制构造,这会导致错误。

我认为编译器选择了非模板重载,导致了编译错误。因为它不是一个模板,所以它的失败是一个错误。在至少gcc上,使用std::move的另一个插入是有效的。

下面是测试代码,用来检查你的编译器是否正确支持此功能:

#include <iostream>
#include <memory>
#include <utility>
#include <type_traits>

class Foo {
};

using namespace std;

int main() {
    cout << is_constructible<pair<const int,unique_ptr<Foo> >, pair<const int,unique_ptr<Foo> >& >::value << '\n';
    cout << is_constructible<pair<const int,unique_ptr<Foo> >, pair<const int,unique_ptr<Foo> >&& >::value << '\n';
}

第一行将输出0,因为复制构造无效。第二行将输出1,因为移动构造有效的。
这段代码:
map.insert(std::move(EntityMap::value_type(entity->getId(), std::move(entity))));

应该调用move insert重载函数。

以下是代码:

map.insert<EntityMap::value_type>(EntityMap::value_type(entity->getId(), std::move(entity))));

真的应该打电话。

编辑:谜团继续,vc返回测试的不正确的11...


在Linux上使用GCC和-std=c++14,我不需要对std::move()进行外部调用。无论是否对std::move()进行外部调用,它都可以正常工作。此外,在EntityMap::value_type前面可能需要关键字typename(取决于您的模板层次结构)。如果是这样,请考虑使用定义本地typedef来提高代码可读性。 - kevinarpe
我简直不敢相信在谷歌上找到展示std::unique_ptrstd::move()std::map一起使用的C++代码示例是多么困难。太不可思议了。这个答案对于修复我的代码中的语法错误非常重要。 - kevinarpe
这个答案的关键是:避免调用 std::make_pair(),而是直接构造 EntityMap::value_type 的实例。 - kevinarpe
@kevinarpe map/set和非可复制的对象(如unique_ptr)的问题在于,当它们作为这些容器中的键时,它们只被视为const,并且在C++中存在一些障碍使得无法以非UB方式删除const不可复制对象——如果它们被移动而没有被编辑(它们是const),则析构函数将对容器中的对象调用一次,对移动的副本调用一次(由于const相同)。例如,智能指针会释放两次等。我在这里询问了相关问题:(https://dev59.com/W2Ei5IYBdhLWcg3wpdnJ) - user3125280
@kevinarpe 我认为使用 make_pair 应该没问题,因为一个临时变量会被绑定到通用引用作为右值 - 但我猜如果你不用 move 包装它,它可能会调用不同的重载函数。 - user3125280
当使用 std::make_pair() 时,我无法使上述代码工作(或非常相似的内容)。这些错误也可能是由于非 const 正确性引起的。 - kevinarpe

1
您的代码与以下内容配合使用:
int main() {
    EntityManager em;
    em.addEntity(std::unique_ptr<Entity>(new Entity(1)));

    return 0;
}

然而这种方法很麻烦,我建议像下面这样定义addEntity:
void EntityManager::addEntity(Entity *entity) {
    if (entity == nullptr) 
        return;
    }

    map.insert(EntityMap::value_type(entity->getId(),
                std::unique_ptr<Entity>(entity)));
}

在HTML中,这句话的意思是“在插入时”。
em.addEntity(new Entity(...));

@Tips48 我不知道它可能是什么,或者确切来自哪里。你需要发布一个编译到你所遇到的错误的示例。 - Ryan Haining
这就是问题所在,它没有给出行号。 - Tips48
错误出在复制构造函数上 - 尽管unique_ptr不可复制构造,但它在代码中的某个地方被调用了 - 因此引用了“已删除”的函数(即复制构造函数)。 - user3125280
我没有可用的编译器/平台,但我假设Ty是值类型,KTy是键类型,因此当某个函数返回键和值的一对时,它会调用复制构造函数(您自己创建这样一对的调用是有效的,因为您使用std::move)- 让我想一想这个问题... - user3125280
以上的回答没有帮助。你当时在想什么? - Tips48
显示剩余9条评论

1

我在使用VS 2017和msvc 14.15.26726时遇到了同样的问题。根据编译器错误日志,似乎与实例化过程中需要std::pair<_kT, _T>的复制构造函数有关。我不知道为什么,但是我发现一个有趣的观察结果(并且可以解决问题),即在map声明之前放置std::unique_ptr的声明,例如:

#pragma once

#include "Entity.h"

#include <map>
#include <memory>

class EntityManager {
private:
    typedef std::unique_ptr<Entity> EntityPtr;
    typedef std::map<int, EntityPtr> EntityMap;
    std::unique_ptr<Entity> aDummyStub; //<-- add this line
    EntityMap map;
//...
};

0

不确定这个解决方案是否也能帮到你,但当我在Visual Studio 2015(更新2)中从静态库切换到动态库时,我突然在一个私有的std :: map<int,std :: unique_ptr<SomeType>>数据成员上遇到了相同的错误。

由于使用模板数据成员与__declspec(dllexport)一起会产生警告(至少在MSVC中),我通过(几乎)应用PIMPL(私有实现)惯用语来解决了该警告。令人惊讶的是,C2280错误也以这种方式消失了。

在你的情况下,可以这样做:

class EntityManagerPrivate {
public:
    EntityMap map;
};

class EntityManager {
private:
    EntityManagerPrivate* d; // This may NOT be a std::unique_ptr if this class 
                             // shall be ready for being placed into a DLL
public:

    EntityManager();
    ~EntityManager();

   // ...
};

而在 .cpp 文件中:

EntityManager::EntityManager() :
    d( new EntityManagerPrivate() )
{
}

EntityManager::~EntityManager()
{
    delete d;
    d = nullptr;
}

// in all other methods, access map by d->map

请注意,对于真正的PIMPL,您必须将私有类移动到一个独立的头文件中,该头文件仅由.cpp引用。实际的头文件只会在包含后声明class EntityManagerPrivate;。 对于真正的PIMPL,私有类除了数据成员外还必须具有实现。

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