具有类型擦除的std :: pair的分段构造

时间:2015-07-05 12:08:40

标签: c++ copy type-erasure std-pair

我正在尝试使用std::unordered_map来存储带有Resource密钥的std::string个对象。 Resource实现类型擦除,以便构造函数Resource(objectOfAnyType)创建一个Resource对象,该对象包装传递给构造函数的对象的类型。

重要的是Resource不被复制,因为它们可能包装不允许复制的对象,或者只支持浅复制,并且当它们被销毁时释放内存。所以我一直在尝试使用std::pair分段构造函数,例如

std::unordered_map<std::string, MyStruct> umap;
umap.emplace(std::piecewise_construct, std::forward_as_tuple("tag"), std::forward_as_tuple(constructorArgs));

这很好用。当我尝试使用实现类型擦除的Resource时,会出现问题。下面是一个完整(但简化)的例子,它重现了我遇到的问题。我意识到代码中存在几个不相关的问题(尤其是在MyStruct中对堆的所有可疑使用),但它不是生产代码,并且已经过大量编辑以通过更简单的示例重现问题。

#include <iostream>
#include <unordered_map>
#include <typeinfo>
#include <memory>

//Resource class providing a handle to generic game resources. Implements type erasure to support indefinite numbers of resource types.
class Resource
{
private:
    //Allows mixed storage in STL containers.
    class ResourceConcept
    {
    public:
        virtual ~ResourceConcept() {  }
    };

    //Templated model of ResourceConcept. Allows type erasure and storing of any type.
    template <class ResourceType>
    class ResourceModel : public ResourceConcept
    {
    private:
        ResourceType modelledResource;
    public:
        ResourceModel(const ResourceType &_resource) : modelledResource(_resource) {  }
        virtual ~ResourceModel() {  }
    };

    //Unique pointer to the resource itself. Points to an object of type ResourceConcept allowing any specific instantiation of the templated ResourceModel to be stored.
    std::unique_ptr<ResourceConcept> resource;

    //Uncommenting the two lines below causes an error, because std::pair is trying to copy Resource using operator=.
    //Resource(const Resource* _other) = delete;
    //Resource& operator= (const Resource&) = delete;

public:
    //Constructor which initialises a resource with whichever ResourceModel is required.
    template <class ResourceType>
    Resource(const ResourceType& _resource) : resource(new ResourceModel<ResourceType>(_resource)) { std::cout << "Resource constructed with type " << typeid(_resource).name() << std::endl; }
};

//Example structure which we want to store/model as a Resource.
struct MyStruct
{
    std::string* path;
    MyStruct(std::string _path) : path(new std::string(_path)) { std::cout << "MyStruct constructor called..." << std::endl; }
    ~MyStruct() { std::cout << "MyStruct destructor called!" << std::endl; delete path; } //In a real example, this would deallocate memory, making shallow-copying the object a bad idea.
private:
    MyStruct(const MyStruct* _other) = delete;
    MyStruct& operator= (const MyStruct&) = delete;
};

int main()
{
    std::unordered_map<std::string, Resource> umap;
    std::string constructorArgs = "Constructor argument.";

    //Store a MyStruct in the map using Resource(MyStruct) constructor...
    umap.emplace(std::piecewise_construct, std::forward_as_tuple("tag1"), std::forward_as_tuple(constructorArgs)); //Calls Resource(std::string), which isn't what I want.
    umap.emplace(std::make_pair("tag2", Resource(MyStruct(constructorArgs)))); //Calls a Resource(MyStruct), results in the MyStruct destructor being called twice! Example output below.

    std::cout << "tag1: " << typeid(umap.at("tag1")).name() << "\ttag2: " << typeid(umap.at("tag2")).name() << std::endl;
    std::cout << "End test." << std::endl;

    /*
    Example output:

    Resource constructed with type Ss
    MyStruct constructor called...
    Resource constructed with type 8MyStruct
    MyStruct destructor called!      <--- I need to prevent this call to the destructor.
    tag1: 8Resource tag2: 8Resource
    End test.
    MyStruct destructor called!
    Segmentation fault
    */

    return 0;
}

主要问题是MyStruct似乎被复制,一个副本被销毁。在实际代码中,这将导致指针被释放,而另一个副本带有悬空指针(因此seg错误)。

在使用类型擦除时是否可以构造就地/分段对?它是std :: move可以帮助的东西吗?一种解决方案可能是使用std::shared_ptr,但如果我不必要地复制Resource,我宁愿直接对其进行排序。

感谢您的帮助。

1 个答案:

答案 0 :(得分:1)

如果您不想要复制资源,则应禁止在您的资源上使用。您尝试过但错误地说:您删除的副本构造函数(在MyStruct和Resource上)采用const指针,但它应该是const references,如

MyStruct(const MyStruct& _other) = delete;
Resource(const Resource& _other) = delete;

一旦你使用它,你就不再冒险使用segfault,因为MyStruct不能再被复制了,但你会得到一堆编译器错误,因为其余的代码想要复制。如您所述,输入移动构造函数,右值引用和std :: move:我建议您在网上或SO上阅读它,直到您完全理解它为止。

应用于您的特定代码,它需要进行以下更改:移动MyStruct的构造函数(最终也适用于Resource,但不会在您显示的代码中使用),并且Resource构造函数必须对资源进行rvalue引用,以便它可以从它移动:

MyStruct MyStruct&& rh) :
  path(rh.path)
{
  rh.path = nullptr;
}

template <class ResourceType>
Resource(ResourceType&& _resource) :
  resource(new ResourceModel<ResourceType>(std::forward<ResourceType>(_resource)))
{
  std::cout << "Resource constructed with type " << typeid(resource).name() << std::endl;
}

Resource(Resource&& rh) :
  resource(std::move(rh.resource))
{
}

然后你只需使用

umap.emplace("tag2", Resource(MyStruct(constructorArgs)))