3

我想编写一个ContentManager类来加载和维护游戏的不同类型的资产(与XNA 的 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,将其插入地图并返回它。

我的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_;
};

在测试我的代码时,我意识到每个纹理都被释放了两次,因为对象的(浅)副本是Texture在某个时候创建​​的,当然,每个对象都调用了析构函数。

此外,虽然Load<T>()返回对地图元素的引用,但调用者可以自己制作副本并触发相同的问题。我的想法:

  • 将复制构造函数设为Texture私有不是解决方案,因为在创建std::pair将新元素插入地图时无论如何都需要副本。

  • 定义一个创建深层副本的自定义复制构造函数似乎根本不合理,因为我需要创建底层 Direct3D 纹理的副本,它应该只存在一次。

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

4

5 回答 5

5

在 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。编译器应该检测到这两个,并停止生成隐式复制构造函数和复制赋值,所以你Texture不能再被复制,这正是你想要的,因为深度复制 aIDirect3DTexture9很困难,甚至没有意义。Texture 类现在神奇地修复了。

现在,其他类呢?没有变化。std::map<std::string, Texture>足够聪明,可以检测到你的类有noexcept移动操作符,所以它会自动使用它们而不是副本。它还使map自身可移动但不可复制。并且由于map是可移动但不可复制的,这会自动使ContentManager可移动但不可复制。当您考虑时这是有道理的,移动内容很好,但您不想复制所有这些。所以那些不需要任何改变


现在,由于 rvalues 对您来说显然是一个新概念,所以这里有一个速成课程:

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.   
于 2013-08-26T17:53:23.793 回答
1

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

于 2013-08-26T17:41:00.150 回答
0

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

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

于 2013-08-26T17:43:47.493 回答
0

是的,我建议为此使用智能指针。特别是您可以使用shared_ptr根据您的编译器版本或可用的框架,您可能想要使用C++11 变体或例如Boost 的 implementation

于 2013-08-26T17:44:32.663 回答
0

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

  1. 您忘记了三巨头的规则:如果您编写了复制构造函数、复制赋值运算符或析构函数中的任何一个,那么您很可能应该编写另外两个。在这种情况下,将它们设为私有
  2. 在 a 中插入元素的方法有很多种mapinsert这只是其中一种

纯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_constructpair的构造函数,如textures_.emplace(std::piecewise_construct, std::make_tuple(assetName), std::make_tuple(...));
于 2013-08-26T18:07:08.123 回答