std::shared_ptr在空指针上调用非默认删除器

6

看下面这个例子:

#include <iostream>
#include <memory>

class Foo {
public:
    Foo()  { std::cout << "Foo()\n";  }
    ~Foo() { std::cout << "~Foo()\n"; }
};

int main(){
    auto deleter = [](Foo* p) {
        if(!p) { std::cout << "Calling deleter on nullptr\n"; }
        delete p;
    };

    std::shared_ptr<Foo> foo;
    std::cout << "\nWith non-null Foo:\n";
    foo = std::shared_ptr<Foo>(new Foo, deleter);
    std::cout << "foo is " << (foo ? "not ":"") << "null\n";
    std::cout << "use count=" << foo.use_count() << '\n';
    foo.reset();

    std::cout << "\nWith nullptr and deleter:\n";
    foo = std::shared_ptr<Foo>(nullptr, deleter);
    std::cout << "foo is " << (foo ? "not ":"") << "null\n";
    std::cout << "use count=" << foo.use_count() << '\n';
    foo.reset();

    std::cout << "\nWith nullptr, without deleter:\n";
    foo = std::shared_ptr<Foo>(nullptr);
    std::cout << "foo is " << (foo ? "not ":"") << "null\n";
    std::cout << "use count=" << foo.use_count() << '\n';
    foo.reset();
}

输出结果为:
With non-null Foo:
Foo()
foo is not null
use count=1
~Foo()

With nullptr and deleter:
foo is null
use count=1
Calling deleter on nullptr

With nullptr, without deleter:
foo is null
use count=0

在这里,我们可以看到当shared_ptr使用 nullptr和自定义删除器进行初始化时,它会调用所包含的删除器。

似乎当使用自定义删除器初始化时,shared_ptr认为它“拥有” nullptr,因此在删除任何其他拥有的指针时尝试删除它。但如果没有指定删除器,则不会发生这种情况。

这是预期行为吗?如果是,这种行为背后的原因是什么?


1
delete(nullptr) is, afaik, completely valid, so it makes sense that, for any value given to a shared_ptr, it'll call the deleter it's given, without wasting cycles checking if it's nullptr - Phil M
6
与std::unique_ptr不同,即使管理的指针为空,std::shared_ptr的删除器也会被调用。 - Samer Tufail
相关: https://dev59.com/Qobca4cB1Zd3GeqPcO5t - Richard Hodges
@PhilM,实际上这并不是标准规定,但根据as-if规则(当没有提供删除器时),它可能会像您描述的那样执行。 - Lightness Races in Orbit
哦,刚意识到 https://dev59.com/rGgu5IYBdhLWcg3wrIuR#11164463 可能是重复的。 - Lightness Races in Orbit
显示剩余2条评论
1个回答

6

tl;dr:是的,这是有意的。


这相当微妙。

shared_ptr 可以处于两种状态:

使用空指针构造 shared_ptr 实际上会使其成为非空!get() 返回 p 意味着 get() 返回 nullptr,但这并不意味着它为空。

由于默认删除器只执行 delete p,而 delete nullptr 是一个空操作,所以这通常没有关系。但是,正如您所见,如果提供自己的删除器,您可以观察到这种差异。

我不确定为什么会这样。一方面,我可以理解在空指针情况下防止调用deleter的原因,因为通常认为shared_ptr(nullptr)是“空的”(尽管它实际上不是); 另一方面,如果deleter想要做出这个决定(伴随着分支开销),我也可以理解让它自己决定的原因。
你在这里包括了一个检查null的步骤,是正确的。

来自 [util.smartptr.shared.const] 的一些法律术语:

template<class Y, class D> shared_ptr(Y* p, D d);
template<class Y, class D, class A> shared_ptr(Y* p, D d, A a);
template<class D> shared_ptr(nullptr_t p, D d);
template<class D, class A> shared_ptr(nullptr_t p, D d, A a);

9) 要求:构造函数中的 d 和类型为 D 的删除器在使用 std::move(d) 进行初始化时不应抛出异常。表达式 d(p) 应具有明确定义的行为且不应抛出异常。A 必须满足 Cpp17Allocator 要求(表 34)。

10) 效果:构造一个拥有对象 p 和删除器 dshared_­ptr 对象。当 T 不是数组类型时,第一和第二个构造函数使得 shared_­from_­this 可以使用 p。第二和第四个构造函数将使用 a 的副本来分配内部使用的内存。如果抛出异常,则调用 d(p)

11) 确保:use_­count() == 1 && get() == p

(请注意,即使是 !p 的情况也没有例外。)

[util.smartptr.shared.dest] 中可以看出:

~shared_ptr();

1) 效果:

  • 如果 *this 为空或与另一个 shared_­ptr 实例共享所有权(use_­count() > 1),则没有副作用。
  • 否则,如果 *this 拥有对象 p 和删除器 d,则调用 d(p)
  • 否则,*this 拥有指针 p,并调用 delete p

附注:我认为上述段落中“拥有对象”和“拥有指针”这两个短语之间的混淆是编辑问题。


我们还可以在cppreference.com的~shared_ptr文章中看到这一点记录:

std::unique_ptr不同,即使管理的指针为空,std::shared_ptr的删除器也会被调用。

(请使用文档!)

template<class Y, class D> shared_ptr(Y* p, D d); mandates that delete p is a valid op and by defeault delete nullptr is. If the custom deleter can't handle that then it doesn't make sense to pass nullptr to sharedptr - Samer Tufail
如果使用别名构造函数创建了一个空的shared_ptr,则其存储指针可能是非空的 - 请参见http://eel.is/c++draft/util.smartptr.shared#const-17。 - yachoor
@SamerTufail 正确。 - Lightness Races in Orbit
@yachoor 谢谢! - Lightness Races in Orbit
一个空的 shared_ptr 可以用来执行任意代码:shared_ptr<void> guard(static_cast<void*>(0), bind(f, x, y)) - 来自 https://www.boost.org/doc/libs/1_69_0/libs/smart_ptr/doc/html/smart_ptr.html#techniques_using_shared_ptr_to_execute_code_on_block_exit - yachoor
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接