代码之家  ›  专栏  ›  技术社区  ›  ph4nt0m

C++:存储复制操作不合理或不可能的资源

  •  3
  • ph4nt0m  · 技术社区  · 11 年前

    我想写一篇 ContentManager 为游戏加载和维护不同类型资产的类(与 XNA's ContentManager ). 我的头文件如下所示:

    class ContentManager
    {
    public:
      ContentManager(Direct3D& d3d, const std::string& rootDirectory = "Resource");
     ~ContentManager();
    
      template<typename T>
      const T& Load(const std::string& assetName);
    
    private:
      Direct3D& d3d_;
      std::string rootDirectory_;
    
      std::map<std::string, Texture> textures_;
    };
    

    正如你所看到的,我有一个每个资源类型的贴图(目前只针对纹理)和一个通用 Load<T>() 方法,我为要存储的每个资产类型显式实例化该方法。 Load<Texture>() 从磁盘中读取一个图像文件(如果它不在地图中),创建一个新的 Texture ,将其插入到地图中并返回。

    我的 纹理 类基本上创建并封装一个原始 IDirect3DTexture9 遵循 RAII 习惯用法(析构函数调用 Release() texture_ ):

    class Texture
    {
    public:
      Texture();
      Texture(Direct3D& d3d, unsigned int width, unsigned int height, 
        const std::vector<unsigned char>& stream);
    
      ~Texture();
    
      IDirect3DTexture9* GetD3DTexture() const { return texture_; }
    
    private:
      IDirect3DTexture9* texture_;
    };
    

    在测试我的代码时,我意识到每个纹理都发布了两次,因为 纹理 对象是在某个时刻创建的,当然,每个对象都会调用析构函数。

    此外,尽管 加载<T>() 返回对映射元素的引用,调用方可以自己进行复制并触发相同的问题。我的想法:

    • 正在制作的复制构造函数 纹理 private不是解决方案,因为在创建 std::pair 在地图中插入一个新元素。

    • 定义一个自定义副本构造函数来创建深度副本似乎一点也不合理,因为我需要创建底层Direct3D纹理的副本,而它实际上应该只存在一次。

    那么我有什么选择呢?这可以通过使用智能指针来解决吗?应将哪种类型存储在地图中,以及应将哪一种类型存储 加载<T>() 回来

    5 回复  |  直到 11 年前
        1
  •  5
  •   Mooing Duck    11 年前

    在C++11中,该语言添加了一个称为移动的功能 确切地 你遇到的原因。幸运的是,修改代码以使用移动机制非常简单:

    class Texture
    {
    public:
      Texture() noexcept;
      Texture(Direct3D& d3d, unsigned int width, unsigned int height, 
        const std::vector<unsigned char>& stream);
    
      Texture(Texture&& rhs) noexcept
      : texture_(rhs.texture_) //take the content from rhs
      {rhs.texture_ = nullptr;} //rhs no longer owns the content
    
      Texture& operator=(Texture&& rhs) noexcept
      {
        Clear(); //erase previous content if any
        texture_ = rhs.texture_; //take the content from rhs
        rhs.texture_ = nullptr; //rhs no longer owns the content
        return *this;
      }
    
      ~Texture() noexcept {Clear();}
    
      IDirect3DTexture9* GetD3DTexture() const noexcept { return texture_; }
    
    private:
      void Clear() noexcept; //does nothing if texture_ is nullptr
      IDirect3DTexture9* texture_;
    };
    

    这添加了一个“移动构造函数”和一个“运动赋值” 移动 来自一个的内容 Texture 到另一个,所以只有一个指向给定的 IDirect3DTexture9 一次。编译器应该检测到这两个,并停止生成隐式复制构造函数和复制赋值,因此 纹理 无法再复制,这正是您想要的,因为深度复制 IDirect3D纹理9 很难,甚至没有真正的意义。纹理类现在被神奇地修复了。

    现在,其他课程呢?没有变化。 std::map<std::string, Texture> 足够聪明,可以检测到您的类 noexcept 移动运算符,因此它将自动使用它们而不是副本。它还使 map 它本身是可移动的,但不能复制。自从 地图 可移动但不可复制,自动制造 ContentManager 可移动但不可复制。仔细想想,移动内容是有道理的,但你不想 复制 所有这些。所以这些不需要任何改变


    现在,由于右值对你来说显然是一个新概念,这里有一个速成课程:
    Texture getter(); //returns a temporary Texture
    Texture a = getter(); //since the right hand side is a temporary,
                            //the compiler will try to move construct it, and only 
                            //copy construct if it can't be moved.
    a = getter(); //since the right hand side is a temporary,
                            //the compiler will try to move assign it, and only 
                            //copy assign if it can't be moved.
    
    void setter1(Texture&); //receives a reference to an outside texture object
    setter1(a); //works exactly as it always has
    setter1(getter()); //compiler error, reference to temporary
    setter1(std::move(a)); //compiler error, passing rreference && instead of lreference &.
    
    void setter2(Texture); //receives a local texture object
    setter2(a); //compiler error, no copy constructor
    setter1(getter()); //creates the Texture by moving the data from the temporary
    setter2(std::move(a)); //The compiler moves the content from a to the function;
                          //the function receives the content previously held by a, and 
                          //a now has no content.  Careful not to read from a after this.
    
    void setter3(Texture&&); //receives a reference to a temporary texture
    setter3(a); //compiler error, a is not a temporary
    setter3(getter()); //function receives reference to the temporary, all is good
    setter3(std::move(a)); //function thinks a is temporary, and will probably remove 
                           //it's content. Careful not to read from a after this.   
    
        2
  •  1
  •   Yochai Timmer    11 年前

    将构造函数设为私有。
    制作一个包含指向对象而不是对象本身的指针的贴图。

        3
  •  0
  •   MasterPlanMan    11 年前

    最好是为每个对象保留一个引用计数器。因此,我将使用共享指针(std::map<std::string,std::tr1::shared_ptr<…>>)。

    您也可以尝试使用内容管理器来释放对象(即:release(T&object)),并在映射本身中保留一个引用计数器。但最好的是共享指针。。

        4
  •  0
  •   kossmoboleat    11 年前

    是的,我建议使用智能指针。特别是你可以使用 shared_ptr 根据您的编译器版本或可用的框架,您可能希望使用 C++11 variety 或例如。 Boost's implementation .

        5
  •  0
  •   Matthieu M.    11 年前

    答案就在你的指尖:不要使用 insert :)

    1. 你忘记了三巨头的规则:如果你写复制构造函数、复制赋值运算符或析构函数中的任何一个,你很可能应该写另外两个。在这种情况下,请将其保密
    2. 有多种方法可以在 map , 插入 只是其中之一

    纯C++03溶液:

    class Texture
    {
    public:
      Texture(): texture_(0) {}
    
      Load(Direct3D& d3d, unsigned int width, unsigned int height, 
           const std::vector<unsigned char>& stream);
    
      ~Texture();
    
      IDirect3DTexture9* GetD3DTexture() const { return texture_; }
    
    private:
      IDirect3DTexture9* texture_;
    };
    
    
    template<typename T>
    T const& ContentManager::Load(const std::string& assetName) {
        Texture& t = textures_[assetName];
    
        t.Load(....);
    
        return ...;
    }
    

    现在,首先将对象放置在映射中(使用其默认构造函数),然后 然后 实际填充。

    在C++11中,您还有两种选择:

    • 定义一个移动构造函数以便能够重新定位对象
    • 使用 emplace 并瞄准 piecewise_construct 中的对的构造函数 textures_.emplace(std::piecewise_construct, std::make_tuple(assetName), std::make_tuple(...));