我使用指针已经有几年了,但是直到最近我才决定过渡到C++11的智能指针(即unique,shared和Weake)。我对它们做了大量的研究,得出的结论如下:
因此,考虑到这些原则,我开始修改我的代码库,以利用我们新的闪亮的智能指针,完全打算清除尽可能多的原始指针。然而,对于如何最好地利用C++11智能指针,我感到困惑。
让我们假设,例如,我们正在设计一个简单的游戏。我们决定最好将虚构的纹理数据类型加载到TextureManager类中。这些纹理是复杂的,因此不可能按值传递它们。此外,让我们假设游戏对象需要特定的纹理,这取决于它们的对象类型(例如汽车,船等)。
在此之前,我会将纹理加载到一个向量(或其他容器,如unordered_map)中,并在每个游戏对象中存储指向这些纹理的指针,这样当它们需要渲染时,它们就可以引用它们。让我们假设纹理被保证比它们的指针活得更久。
那么,我的问题是如何在这种情况下最好地利用智能指针。我看到几种选择:
>
然而,我对此的理解是,将从传递的引用复制构造一个新的纹理,然后该纹理将归unique_ptr所有。这对我来说是非常不可取的,因为我会有和使用它的游戏对象一样多的纹理副本--挫败指针的指向(没有双关语的意思)。 不要直接存储纹理,而是在容器中存储它们的共享指针。使用make_shared初始化共享指针。在游戏对象中构造弱指针。 与unique_ptr不同,我不必复制构造纹理本身,但是渲染游戏对象是昂贵的,因为我必须每次锁定weak_ptr(就像复制构造一个新的shared_ptr一样复杂)。 因此,总结一下,我的理解是这样的:如果我要使用独特的指针,我将不得不复制构建文本;或者,如果我要使用共享指针和弱指针,那么每次绘制游戏对象时,我必须基本上复制构建共享指针。 我知道智能指针本来就会比原始指针复杂,所以我肯定会在某些地方有所损失,但这两个成本似乎都比应该的要高。 谁能告诉我正确的方向吗? 很抱歉阅读了这么长的时间,谢谢您的时间!class TextureManager {
public:
const Texture& texture(const std::string& key) const
{ return textures_.at(key); }
private:
std::unordered_map<std::string, Texture> textures_;
};
class GameObject {
public:
void set_texture(const Texture& texture)
{ texture_ = std::unique_ptr<Texture>(new Texture(texture)); }
private:
std::unique_ptr<Texture> texture_;
};
class TextureManager {
public:
const std::shared_ptr<Texture>& texture(const std::string& key) const
{ return textures_.at(key); }
private:
std::unordered_map<std::string, std::shared_ptr<Texture>> textures_;
};
class GameObject {
public:
void set_texture(const std::shared_ptr<Texture>& texture)
{ texture_ = texture; }
private:
std::weak_ptr<Texture> texture_;
};
即使在C++11中,原始指针作为对象的非所有者引用仍然是完全有效的。在你的例子中,你说的是“让我们假设纹理被保证比它们的作者更长寿。”这意味着你完全可以安全地使用原始指针到游戏对象中的纹理。在纹理管理器中,可以自动存储纹理(保存在保证内存中位置恒定的容器中),或者保存在 如果outlive-the-pointer保证无效,将纹理存储在管理器中的
总结一下您的用例:*)对象被保证比用户的寿命长*)对象一旦被创建,就不会被修改(我想这是您的代码所暗示的)*)对象可以通过名称被引用,并且被保证以您的应用程序所要求的任何名称存在(我在外推--如果这不是真的,我将在下面讨论该怎么做。)
这是一个令人愉快的用例。您可以在整个应用程序中对纹理使用值语义!这样做的优点是性能好,容易推理。
一种方法是让你的TextureManager返回一个纹理const*。考虑:
using TextureRef = Texture const*;
...
TextureRef TextureManager::texture(const std::string& key) const;
因为下面的纹理对象具有应用程序的生存期,从不被修改,并且始终存在(指针从不为nullptr),所以您可以将TextureRef视为简单值。您可以传递它们,返回它们,比较它们,并制作它们的容器。它们很容易推理,而且工作效率很高。
这里令人烦恼的是,您有值语义(这是好的),但是指针语法(对于具有值语义的类型来说,这可能会令人困惑)。换句话说,要访问纹理类的成员,您需要执行如下操作:
TextureRef t{texture_manager.texture("grass")};
// You can treat t as a value. You can pass it, return it, compare it,
// or put it in a container.
// But you use it like a pointer.
double aspect_ratio{t->get_aspect_ratio()};
处理这一问题的一种方法是使用类似pimpl习惯用法的东西,并创建一个类,该类只不过是指向纹理实现的指针的包装器。这是一个更多的工作,因为您最终将为您的纹理包装器类创建一个API(成员函数),该API转发到您的实现类的API。但好处是您有一个同时具有值语义和值语法的纹理类。
struct Texture
{
Texture(std::string const& texture_name):
pimpl_{texture_manager.texture(texture_name)}
{
// Either
assert(pimpl_);
// or
if (not pimpl_) {throw /*an appropriate exception */;}
// or do nothing if TextureManager::texture() throws when name not found.
}
...
double get_aspect_ratio() const {return pimpl_->get_aspect_ratio();}
...
private:
TextureImpl const* pimpl_; // invariant: != nullptr
};
。。。
Texture t{"grass"};
// t has both value semantics and value syntax.
// Treat it just like int (if int had member functions)
// or like std::string (except lighter weight for copying).
double aspect_ratio{t.get_aspect_ratio()};
我假设在你的游戏中,你永远不会要求一个不能保证存在的纹理。如果是这种情况,那么您可以断言该名称存在。但如果情况并非如此,那么你需要决定如何处理这种情况。我的建议是将指针不能为nullptr作为包装类的一个不变量。这意味着如果纹理不存在,则从构造函数抛出。这意味着您可以在创建纹理时处理这个问题,而不是每次调用包装器类的成员时都要检查空指针。
在回答您最初的问题时,智能指针对于生存期管理很有价值,如果您所需要的只是将引用传递到其生存期保证超过指针的对象,那么智能指针就没有特别的用处。
您可以有一个std::unique_ptrs的std::map,用于存储纹理。然后您可以编写一个get方法,通过名称返回对纹理的引用。这样,如果每个模型都知道其文本的名称(它应该知道),您就可以简单地将该名称传递到get方法中,并从映射中检索一个引用。
class TextureManager
{
public:
Texture& get_texture(const std::string& key) const
{ return *textures_.at(key); }
private:
std::unordered_map<std::string, std::unique_ptr<Texture>> textures_;
};
然后你可以在游戏对象类中使用纹理,而不是Texture*,weak_ptr等。
这样纹理管理器就可以像缓存一样工作,get方法可以被重新编写来搜索纹理,如果找到了就从地图返回它,否则首先加载它,将它移动到地图,然后返回一个对它的引用