空别名 shared_ptr 是无操作删除 shared_ptr 的一个很好的替代方案吗?

Posted

技术标签:

【中文标题】空别名 shared_ptr 是无操作删除 shared_ptr 的一个很好的替代方案吗?【英文标题】:Is an empty aliasing shared_ptr a good alternative to a no-op deleting shared_ptr? 【发布时间】:2015-07-26 12:19:09 【问题描述】:

有时我需要具有无操作删除器的 shared_ptr 实例,因为 API 需要一个 shared_ptr 实例,它想在有限的时间内存储它,但我得到了一个我不允许拥有的原始指针比我要争取的时间更长。

对于这种情况,我一直在使用无操作删除器,例如[](const void *),但今天我发现还有另一种替代方法,使用(或滥用?)aliasing constructor 的shared_ptr

void f(ExpectedClass *ec) 
   std::shared_ptr<ExpectedClass> p(std::shared_ptr<void>(), ec);
   assert(p.use_count() == 0 && p.get() != nullptr);
   apiCall(p);

我的问题是,有什么更好的方法来做到这一点,为什么?性能预期是否相同?对于无操作删除器,我希望为删除器和引用计数的存储支付一些成本,而在使用带有空 shared_ptr 的别名构造函数时似乎并非如此。

【问题讨论】:

是的,在 [util.smartptr.shared.const]/16 中有一个注释提到了这个奇怪的地方。恕我直言,这是您可以使用非空shared_ptr 存储空值(shared_ptr&lt;T&gt;((T*)nullptr))这一事实的必然结果。可能有点令人困惑,但有效。这种用法的一个问题是p 的副本不与它共享所有权,因为它们都不拥有任何东西。这对于希望检查对象共享所有权(通过owner_less)的某些用途可能很重要,但对于您描述的用途可能不是问题。 @JonathanWakely 感谢您的留言。它之前的注释/15)让我感到困惑。为什么用户必须确保p 一直有效,直到r 的所有权组在shared_ptr&lt;&gt;(r, p) 中被销毁?我认为这是shared_ptr 的业务。如果我只是将它存储起来,别名指针将始终有效,因为它可以防止 r 的所有权组被破坏,以便用户可以“复制并忘记”它,还是我错过了什么? 啊,我明白它的意思了。如果p 的生命周期不绑定r 的生命周期(这是允许的),那么r 的生命周期并不意味着p 的生命周期,那么用户必须保证这一点。 别名构造函数是noexceptshared_ptr的默认ctor也是如此。没有内存分配。 @JohannesSchaub-litb,是的,我相信结果是等价的。对于第一个问题,您的后续评论完全正确。考虑struct A int i; ; void foo() auto p1 = make_shared&lt;A&gt;(); shared_ptr&lt;int&gt; p2(p1, p1-&gt;i); int i; shared_ptr&lt;int&gt; p3(p1, i); return p3; ,这里p2 满足该要求,只要p1 的所有权组有效,p1-&gt;i 就有效,但p3 不满足该要求,局部变量i 之前超出范围所有权组的使用计数达到零,导致 *foo() 未定义行为。这就是 /15 中的注释的意思。 【参考方案1】:

关于性能,以下基准显示不稳定的数据:

#include <chrono>
#include <iostream>
#include <limits>
#include <memory>

template <typename... Args>
auto test(Args&&... args) 
    using clock = std::chrono::high_resolution_clock;
    auto best = clock::duration::max();

    for (int outer = 1; outer < 10000; ++outer) 
        auto now = clock::now();

        for (int inner = 1; inner < 20000; ++inner)
            std::shared_ptr<int> sh(std::forward<Args>(args)...);

        auto time = clock::now()-now;
        if (time < best) 
            best = time;
            outer = 1;
        
    

    return best.count();


int main()

    int j;

    std::cout << "With aliasing ctor: " << test(std::shared_ptr<void>(), &j) << '\n'
              << "With empty deleter: " << test(&j, [] (auto) );

在我的机器上输出clang++ -march=native -O2:

With aliasing ctor: 11812
With empty deleter: 651502

具有相同选项的 GCC 提供了更大的比率,5921:465794。 而带有-stdlib=libc++ 的 Clang 产生了惊人的 12:613175。

【讨论】:

事实上,使用 libc++,shared_ptr 部分在使用别名 hack 时被完全优化掉:coliru.stacked-crooked.com/a/84b6d94a03d180b5 @JohannesSchaub-litb ...它发出的语言是什么? 它是转换为 x86 程序集之前的中间语言 (LLVM IR)。我发现更容易卡住,尤其是循环,因为它仍然是 SSA 形式。 区别在于第一个不分配新内存(它只存储一个指向已经存在的控制块的指针),而第二个每次创建共享指针时分配一个新的控制块。跨度> @IlyaPopov 不,默认构造一个 shared_ptr 不会分配任何东西。【参考方案2】:

快速长凳与

#include <memory>

static void aliasConstructor(benchmark::State& state) 
  for (auto _ : state) 
    int j = 0;
    std::shared_ptr<int> ptr(std::shared_ptr<void>(), &j);
    benchmark::DoNotOptimize(ptr);
  


BENCHMARK(aliasConstructor);

static void NoOpDestructor(benchmark::State& state) 
  for (auto _ : state) 
    int j = 0;
    std::shared_ptr<int> ptr(&j, [](int*));
    benchmark::DoNotOptimize(ptr);
  


BENCHMARK(NoOpDestructor);

给予

a ratio of 1/30 for gcc 10.2 a ratio of 1/25 for clang 11 libc++

所以别名构造函数获胜。

【讨论】:

以上是关于空别名 shared_ptr 是无操作删除 shared_ptr 的一个很好的替代方案吗?的主要内容,如果未能解决你的问题,请参考以下文章

shared_ptr 上的原子操作

删除 null void* 指针是未定义的行为吗?

Linux系统基本操作命令 -2

[C++11]共享智能指针shared_ptr指定删除器

boost---shared_ptr笔记

智能指针原理及实现- shared_ptr