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

Piecewise construction of std::pair with type erasure

我正在尝试使用 std::unordered_map 通过 std::string 键存储 Resource 对象。 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好像被复制了,一份被销毁了。在实际代码中,这会导致指针被释放,使另一个副本带有悬空指针(因此出现段错误)。

是否可以在使用类型擦除时就地构造/分段对? std::move 有什么可以帮助的吗?一种解决方案可能是使用 std::shared_ptrs,但如果我无论如何都在不必要地复制 Resources,我宁愿直接解决这个问题。

感谢您的帮助。

如果您无论如何都不希望资源被复制,您应该在您的资源上禁止它。您尝试过但不正确:您删除的复制构造函数(在 MyStruct 和 Resource 上)采用 const 指针,但它应该是 const references like

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

一旦你使用它,你就不会再冒段错误的风险,因为 MyStruct 不能有效地再被复制,但是你会得到一堆编译器错误,因为代码的其余部分想要复制。正如您已经提到的,输入移动构造函数、右值引用和 std::move:我建议您在网上阅读它或 SO,直到您完全理解它。

应用于您的特定代码,它需要这些更改:为 MyStruct 移动构造函数(最终也为 Resource 移动构造函数,但它未在您显示的代码中使用)并且 Resource 构造函数必须采用右值引用资源,以便它可以从中移动:

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)))