英文:
How to use smart pointers?
问题
我知道在C++中,智能指针是处理指针的更好方式:它们会在满足条件时自动删除对象。但这里的条件有点复杂。例如,根据https://en.cppreference.com/w/cpp/memory
std::unique_ptr
在唯一指针超出范围或明确删除时销毁该对象。在第一种情况下,它与仅使用普通的非动态对象有什么不同(我们在堆栈上创建的那个将自动删除)。对于第二种情况,当我们必须手动删除它时,它就不再智能了。
std::shared_ptr
有点类似。
现在我的问题是,如果智能指针被自动删除,那么我们如何将指针传递给另一个函数呢?动态内存分配的整个目的是能够在函数之外使用对象。或者是我误解了“超出范围”的概念吗?
对此的任何良好文档都会受到赞赏。我还没有看到关于这个主题的好文档。
英文:
I know that in C++, a smart pointer is a better way to handle pointers: they automatically delete the object when the conditions are met. But the conditions here are a bit complicated. For example, according to https://en.cppreference.com/w/cpp/memory
std::unique_ptr
disposes of that object when the unique_ptr goes out of scope or we explicitly delete it. In the first case, what makes it different than just using a normal non-dynamic object (the one we create on stack that will automatically be deleted). For the second case, well, it's not smart anymore when we have to manually delete it
std::shared_ptr
is kind of the same
Now my question is, if the smart pointer gets deleted automatically, how can we pass the pointer to another function? The whole point of dynamic memory allocation is to be able to use the object outside of the function after all. Or did I misunderstand the idea of "out of scope"?
Any good documentation on this is appreciated. I haven't seen any good documentation on this subject
答案1
得分: 3
如果可以使用本地对象而不是指针,那几乎总是更好的。但在许多情况下,本地对象不是一个选择,例如,如果你想要多态地创建一个对象,如果你希望对象的生命周期与创建它的范围不同等等。
当我们不得不手动删除它时,它不再智能。
你有手动删除的选择,重要的智能部分是,如果你不手动删除它,它将在销毁时自动删除。
我们如何将指针传递给另一个函数。
你可以通过将指针移动到另一个位置来传递(unique_ptr
)指针的所有权,或者通过将其复制到另一个位置来共享所有权(shared_ptr
)。因此,所指向的对象的生存期不必与原始智能指针对象相同。
例如:
struct Foo
{
virtual ~Foo() {}
};
struct IFactory
{
virtual ~IFactory() {}
virtual std::unique_ptr<Foo> make_foo() = 0;
};
struct Bar
{
void setFactory(std::unique_ptr<IFactory>&& factory)
{
mFactory = std::move(factory);
}
void useFoo()
{
std::unique_ptr<Foo> foo = mFactory->make_foo();
// 对 foo 做一些操作
// foo 现在会在销毁时自动删除
}
std::unique_ptr<IFactory> mFactory;
};
struct FooImpl : Foo
{};
struct FooFactory : IFactory
{
std::unique_ptr<Foo> make_foo() override
{
return std::make_unique<FooImpl>();
}
};
int main()
{
// bar 不需要是指针,所以只需使用本地对象
Bar bar;
bar.setFactory(std::make_unique<FooFactory>());
bar.useFoo();
// 当 bar 被销毁时,工厂会自动销毁
}
在 main
中创建了多态工厂,它的所有权被转移到 bar
的成员变量中。在调用工厂时,它创建对象,然后将所有权转移到工厂的调用者。
英文:
> what makes it different than just using a normal non-dynamic object
If you can use a local object rather than a pointer then that's almost always better. However in a lot of cases a local object isn't an option e.g. if you want to create an object polymorphically, if you want the object to have a different lifetime to the scope it's created in etc.
> it's not smart anymore when we have to manually delete it
You have the option to delete it manually, the important smart bit is that it'll be deleted automatically on destruction if you don't delete it manually.
> how can we pass the pointer to another function
You can transfer (unique_ptr
) ownership of the pointer by moving it into another location or share ownership (shared_ptr
) by copying it into another location. Therefore the lifetime of the pointed to object doesn't have to be the same as the original smart pointer object.
For example:
struct Foo
{
virtual ~Foo() {}
};
struct IFactory
{
virtual ~IFactory() {}
virtual std::unique_ptr<Foo> make_foo() = 0;
};
struct Bar
{
void setFactory(std::unique_ptr<IFactory>&& factory)
{
mFactory = std::move(factory);
}
void useFoo()
{
std::unique_ptr<Foo> foo = mFactory->make_foo();
// Do something with foo
// foo is now automatically destroyed
}
std::unique_ptr<IFactory> mFactory;
};
struct FooImpl : Foo
{};
struct FooFactory : IFactory
{
std::unique_ptr<Foo> make_foo() override
{
return std::make_unique<FooImpl>();
}
};
int main()
{
// there's no need for bar to be a pointer so just use a local object
Bar bar;
bar.setFactory(std::make_unique<FooFactory>());
bar.useFoo();
// the factory is automatically destroyed when bar is destroyed
}
The polymorphic factory is created in main and it's ownership is transferred into the member variable in bar
. When invoking the factory it creates objects whose ownership is then transferred to the caller of the factory.
答案2
得分: -3
Credit to Josh Cross,该课程的TA
在内部,你可以将智能指针看作是跟踪自身引用数量的对象,并在引用计数减少至零时删除对象。它可能看起来像这样(请注意,此处的语法可能略有不正确,但思想仍然相同,这是我想要传达的):
template <typename T>
class SmartPointer {
static std::unordered_map<T*, unsigned int> counts;
T* internal_ptr = nullptr;
public:
// 创建新的智能指针
SmartPointer() {
T* ptr = new T();
counts[ptr] = 1;
internal_ptr = ptr;
}
// 复制构造函数(见下文)
SmartPointer(SmartPointer<T> &other) {
internal_ptr = other.internal_ptr;
++counts[internal_ptr];
}
// 析构函数,但我们需要删除吗?
~SmartPointer() {
if (--counts[internal_ptr] == 0) {
delete internal_ptr;
}
}
// 重载解引用运算符
T *operator(void) const {
return *internal_ptr;
}
};
让我们逐个部分来解释。
成员变量
首先,我们有成员变量。一个用于跟踪指针(internal_ptr),另一个更有趣。unordered_map
允许我们创建一种类似数组的结构,索引不是整数,而是指针,每个指针都指向一个无符号整数。这允许我们跟踪指向给定对象的智能指针数量。这是静态的,以便所有的 SmartPointer
共享相同的映射(请注意,可能有更好的方法来实现这一点,但这只是为了展示概念)。
构造函数/析构函数
现在,我们有两个构造函数和一个析构函数。常规构造函数创建新对象(通常允许传递参数,但为了方便起见,在此处我省略了它),并在内部存储其值。此外,它将与该对象关联的智能指针数设置为1,因为我们刚刚创建了它。
接下来的构造函数被称为复制构造函数。每当我们需要将此对象复制到其他地方时,将调用此构造函数。这包括:
- 将另一个变量设置为已存在的智能指针(
SmartPointer ptr2 = ptr1;
) - 将智能指针传递给函数作为非引用参数(
foo(ptr);
) - 从函数中返回智能指针(
return ptr
)(从技术上讲,这并不总是正确的,但这牵涉到了不太重要的细节,这里我们假装它是正确的)。
在任何这些情况下,复制构造函数将以源智能指针作为参数调用。因此,我们获取其内部指针,因为我们希望使用相同的指针,但我们还需要将指向它的指针数量增加1,因为现在有一个新的引用。
我们还需要析构函数!它首先将指向内部指针的智能指针数量减少1(因为我们即将被删除)。然后,它检查是否没有剩余的其他指针,如果是,则删除它所引用的对象。
运算符
最后,我们重载了 *
运算符,因此如果我们尝试执行 *ptr
(就像普通指针一样),我们将获得预期的解引用。
回答你最初的问题
那么,回到你最初的问题!在这种情况下,当我们将它传递给另一个函数或其他地方时,它是如何避免被删除的呢?好吧!在这种情况下,它仍然具有原始智能指针,并且会创建一个它的副本(因此 counts
将为2)。它将在此函数中执行其需要的操作,然后返回,这会导致参数超出范围并被释放。这将将计数减少到1,这意味着它不会立即被删除,因为仍然有一个可用的引用!
unique
vs shared
vs weak
我想要在此添加一些附言,因为你对这三种类型的指针不太确定。了解它们之间的区别并能够使用它们非常重要,所以我想强调一下。
unique
unique_ptr
是一种智能指针,只能有一个智能指针指向它。这实际上不是一个“真正”的智能指针,并且不执行上述描述的操作,因为你只能同时拥有一个它。可以将其返回并传递给其他函数(这与尝试将其设置为另一个唯一指针不同),但你只能拥有一个指向某物的唯一指针,永远。
因此,在大多数情况下,这些并不是很有用,因为通常只有一个引用指向某物时,你才会使用智能指针,而不是在某事情变得复杂时使用智能指针。
shared_ptr
shared_ptr
就是我上面描述的那样!它使用引用计数来跟踪指向指针的对象的智能指针数量,并像我上面描述的那样工作... 但存在问题。让我们以二叉搜索树(BST
英文:
Credit to Josh Cross, the TA for the class
Internally, you can think of a smart pointer as keeping track of how many references of itself there are, and it deletes the object when there are none left. It might look something like this (note this syntax is probably somewhat wrong, but the idea is still the same and that's what I'm going for):
class SmartPointer<T> {
static unordered_map<T*, unsigned int> counts;
T* internal_ptr = nullptr;
public:
// Creating a new smart pointer
SmartPointer<T>() {
T* ptr = new T();
counts[ptr] = 1;
internal_ptr = ptr;
}
// The copy constructor (see below)
SmartPointer(SmartPointer<T> &other) {
internal_ptr = other.internal_ptr;
++counts[internal_ptr];
}
// Destructor, but do we need to delete?
~SmartPointer() {
if (--counts[internal_ptr] == 0) {
delete internal_ptr;
}
}
// Overloading the dereference operator
T *operator(void) const {
return *internal_ptr;
}
}
Let's unpack this section by section.
Member Variables
First, we have the member variables. One is obviously to keep track of the pointer (internal_ptr), but the other is a bit more interesting. The unordered_map
allows us to have something like an array, where the indexes aren't integers, but are pointers, and each points to an unsigned integer. This allows us to keep track of how many smart pointers are pointing to a given object. It is static
so that all SmartPointer
s share the same map (note there is probably a better way to do this, but this is just to show the concept).
Constructor/Destructor
Now, we have two constructors and a destructor. The regular constructor creates the new object (normally it would allow for parameters, but for ease here I'm excluding it) and stores its value internally. Additionally, it sets the count for the number of smart pointers associated with that to 1, since we just created it.
The next constructor is known as the Copy Constructor. This constructor is called whenever we need to copy this object to somewhere else. This includes:
- Setting another variable equal to an already existing smart pointer (
SmartPointer ptr2 = ptr1;
) - Passing the smart pointer into a function as a non reference (
foo(ptr);
) - Returning the smart pointer from a function (
return ptr
) (this is technically not always true, but this gets to super specifics that aren't important here so we will just pretend it is here).
In any of these cases, the Copy Constructor will be called with the parameter as the source smart pointer. Thus, we take its internal pointer, since we want the same one, but we also need to increase the count of pointers pointing to it by 1, since there are now too.
We also need the destructor! It will first decrease the number of smart pointers pointing to the internal pointer by 1 (since we are about to be deleted). Then, it checks to see if there are no remaining other pointers, and if so, it will delete the object it is referring to.
Operator
Finally, we have the *
operator overloaded, so if we tried to do *ptr
(just like with a normal pointer), we get the same dereference we are expecting.
Answering your original question
So, onto your original question! How does it not get deleted when we pass it to another function or etc? Well! In this case, it will still have the original smart pointer, and a copy of it will be created (thus the counts
will have 2). It will do whatever it needs to in this function, then return, which causes the parameter to go out of scope and be deallocated. This decreases the count back down to 1, which means it won't be deleted quite yet, since it still has a reference available!
unique
vs shared
vs weak
I want to add an addendum to this since you weren't totally sure about the difference between the three types of pointers. It's really important to understand it and be able to use them at all, so I wanted to highlight this.
unique
A unique_ptr
is a smart pointer that can only have one smart pointer pointing to it. This is actually not a "real" smart pointer, and doesn't do what I described above, because you can only have one of them at a time. It's totally fine to return them and pass them to other functions (it differentiates this versus trying to set it equal to another unique pointer), but you can only have one of them pointing to something, ever. Thus, these aren't really as useful in most situations, because you usually want to use smart pointers when things are complicated, not when there's only ever one reference to something.
shared_ptr
A shared_ptr
is what I described above! It uses reference counting to keep track of how many objects are pointing to the pointer and works like I described above... But there's a problem. Let's take our BST as an example.
class Node {
shared_ptr<Node> parent;
shared_ptr<Node> left, right;
...
}
class BST {
shared_ptr<Node> root;
...
}
int main() {
BST bst;
bst.insert(8);
bst.insert(3);
}
In this example, we have a BST that has two nodes: 8 and 3. The 8 node has a shared_ptr
pointing to 3, and the 3 node has a shared_ptr
pointing to 8 (as its parent). Additionally, the BST
has a pointer to 8, as it is the root. But what happens when bst
goes out of scope? Well, the number of shared_ptrs
pointing to 8 goes from 2 to 1 and... Uh oh. Nothing gets deleted, because we have a circular reference chain here.
So are shared pointers utterly useless? Or is there hope yet (spoiler, there is)?
weak_ptr
A weak_ptr
solves the above problem! A weak pointer is like a shared pointer, in that it keeps track of the pointer internally, but unlike the shared pointers, it does not contribute to the counts
for the pointer. This means that we can basically have an extra pointer that "doesn't count", and only exists for us to traverse around. So, we can fix the code like this:
class Node {
weak_ptr<Node> parent;
shared_ptr<Node> left, right;
...
}
Now, things are a bit different! We will no longer have the parent
pointer contributing to the number of references that still exist. This means that in the former example, we would have one pointer to the 8 node (the root), one pointer to the 3 node (8's left child), and one weak/non-counting pointer to 8 (3's parent).
So, when the BST goes out of scope, the number of pointers pointing to the 8 node will be... Zero! So it will be automatically deallocated! Which makes the number of pointers pointing tot he 3 node also be zero! So it will be deleted, and so on and so forth for the entire tree.
With a setup like this, you do not need an explicit destructor for the BST or Nodes! Everything is taken care of behind the scenes for you.
Thus, a mixture of weak_ptrs
and shared_ptrs
is required when you have any sort of circular referencing, and this can often make it hard (or impossible) to use them in certain situations, unfortunately.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论