一个既可以持有Foo*也可以持有std::shared_ptr<Foo>的类型

7
什么是在同一类型中存储std::shared_ptr或Foo*的最方便方法?
Foo* a = ...;
std::shared_ptr<Foo> b = ...;

Bar c = a; // Success, Bar type can hold Foo*
Bar d = b; // Success, Bar type can also hold std::shared_ptr<Foo>

std::variant<Foo*, std::shared_ptr< Foo>> 可以使用,但直接引用它并不可行,有点麻烦。是否有更好的方法?


Bar 添加一个构造函数,该构造函数接受一个 std::shared_ptr<Foo> - NathanOliver
2
类型 Bar 的所有权语义将是什么?原始指针没有明确的所有权语义,而 shared_ptr 表示共享所有权(并通过其基础自动引用计数实现自动生命周期管理)。 - dfrib
我正在寻找的是条形图类型,如果没有表达清楚,对不起。 - Vladimir Bogachev
条形类型不应更改底层指针的所有权语义。 - Vladimir Bogachev
2
一个 Bar 对象存储了一个 Foo*(裸指针),它是否拥有它(可能)指向的资源?或者 Bar 只是它的观察者,依赖于其他人来处理该资源(但在 Bar 使用完之前不会删除它)? - dfrib
显示剩余3条评论
5个回答

8

只需使用一个std::shared_ptr<Foo>

虽然这很少有用,但实际上您可以构造一个非拥有和非计数的 std::shared_ptr

auto p = std::shared_ptr<Foo>{std::shared_ptr<void>(), raw_pointer};

如果您想迎合那些不尊重抽象概念(具体来说是查看引用计数)的奇怪人,您还可以将一个永久锚点藏在某个地方并使用它:

struct pass_t {
    template <class... T>
    constexpr int operator()(T&&...) noexcept
    { return 0; }
};
constexpr inline pass_t pass;

inline const std::shared_ptr<void> anchor {nullptr, pass};

auto p = std::shared_ptr<Foo>{anchor, raw_pointer};

很好的利用了别名构造函数。虽然我想不出你提到的边角情况可能是什么。 - Quentin
@Quentin 如果有人明确地查看引用计数,他们可能会变得非常困惑... - Deduplicator

3
您可以将std::variant<Foo*, std::shared_ptr<Foo>>包装在一个正确的解引用对象中。
class Bar
{
public:
    explicit Bar(Foo* foo) : foo(foo) {}
    explicit Bar(std::shared_ptr<Foo> foo) : foo(foo) {}

    Foo& operator*() const { return std::visit(deref, foo); }
    Foo* operator->() const { return std::visit(arrow, foo); }

private:
    struct {
        Foo& operator()(Foo* f) { return *f; }
        Foo& operator()(std::shared_ptr<Foo> f) { return *f; }
    } deref;

    struct {
        Foo* operator()(Foo* f) { return f; }
        Foo* operator()(std::shared_ptr<Foo> f) { return f.get(); }
    } arrow;

    std::variant<Foo*, std::shared_ptr<Foo>> foo;
};

1
在调用 std::visit() 时,Bar 操作符缺少 return 语句。 - Remy Lebeau

3

基本上与 Caleth 的回答相同,但更通用并展示了新类型的变体特性:

template <class... PointerTypes>
struct PointerHolder : std::variant<PointerTypes...> {
    using std::variant<PointerTypes...>::variant;
    
    auto *operator -> () const {
        return &operator*();
    }
    
    auto &operator * () const {
        return std::visit([](auto const &p) -> auto & { return *p; }, *this);
    }
};

在 Wandbox 上实时查看


&operator->(); ?? - Asteroids With Wings
@Caleth 傻眼了。我还把它反过来重写了,这样就不会像 *&*actualPointer 一样绕了一个大圈了。 - Quentin

3
使用shared_ptr来管理既有所有权的Foo*,也有无所有权的Foo*。通过使用一个自定义删除器,该删除器什么也不做,您可以选择将其作为哪个指针的所有者。
struct not_owned
{
    void
    operator()(void*) const
    {
    }
};


Foo f;
std::shared_ptr<Foo> sp{&f, not_owned{}};  // sp doesn't own the pointer
sp = std::make_shared<Foo>();              // sp does own the pointer

请注意只在某个包装器内执行此操作,否则它将生成您认为已引用计数但实际上没有的 sp 副本,并且会发生泄漏。 - Asteroids With Wings
这很聪明。你也可以将 not_owned 设计成一个简单的函数(当然要按原样传递它)。 - Quentin

2

变量可能与一些辅助工具配合使用。

template<class X>
struct is_variant: std::false_type{};
template<class...Ts>
struct is_variant< std::variant<Ts...> >:std::true_type{};
template<class X>
constexpr bool is_variant_v = is_variant<X>{};

template<class F>
struct variant_method {
  F f;
  template<class Variant> requires (is_variant_v<std::decay_t<Variant>>)
  friend auto operator->*( Variant && variant, variant_method const& self ) {
    return [&](auto&&...args)->decltype(auto) {
      return std::visit( [&](auto&& val)->decltype(auto) {
        return self.f( decltype(val)(val), decltype(args)(args)... );
      }, std::forward<Variant>(variant) );
    };
  }
};
template<class F>
variant_method(F&&)->variant_method<std::decay_t<F>>;

现在我们可以这样做:
constexpr variant_method dereference{ [](auto&& x)->decltype(auto) { return *x; } };
template<class T>
constexpr variant_method operator_{ [](auto&& x)->T { return static_cast<T>(x); } };
constexpr auto to_bool = operator_<bool>;

现在开始!

struct something {
  int x = 0;
};

std::variant<std::shared_ptr<something>, something*> some_ptr;

some_ptr = new something;
if ((some_ptr->*to_bool)())
  (some_ptr->*dereference)().x = 7;

some_ptr = std::make_shared<something>();

if ((some_ptr->*to_bool)())
  (some_ptr->*dereference)().x = 3;

实时示例

现在,我们可以将其包装得更漂亮。

template<class...Ts>
struct variant_ptr:std::variant<Ts...> {
  using base = std::variant<Ts...>;
  using base::base;

  auto& operator*() const { return (((base const&)*this)->*dereference)(); }
  auto& operator*() { return (((base&)*this)->*dereference)(); }

  auto* get() const { return std::addressof(**this); }
  auto* get() { return std::addressof(**this); }

  auto* operator->() const { return get(); }
  auto* operator->() { return get(); }

  explicit operator bool() const { return (((base const&)*this)->*to_bool)(); }
};

现在我们要做的是:
variant_ptr<Foo*, std::shared_ptr<Foo>>

我們希望一切都“只是工作”。
示例可在此处查看。
當然,我们可以跳过上面的所有variant_method内容,只需在variant_ptr中实现std :: visit。
哦,这里有一个版本,通过向variant_method :: operator-&gt; *教授与从variant类型派生的类型交互的知识来摆脱那些base&amp;强制转换
template<class...Ts>
constexpr decltype(auto) get_variant_base(std::variant<Ts...> const& x) { return x; }
template<class...Ts>
constexpr decltype(auto) get_variant_base(std::variant<Ts...>& x) { return x; }
template<class...Ts>
constexpr decltype(auto) get_variant_base(std::variant<Ts...>&& x) { return std::move(x); }

constexpr auto get_variant_base_lambda = [](auto&&x)->decltype(get_variant_base(decltype(x)(x))){ return get_variant_base(decltype(x)(x)); };

template<class X>
constexpr bool is_variant_derived_v = std::is_invocable_v< decltype(get_variant_base_lambda), X >;

template<class F>
struct variant_method {
  F f;
  template<class Variant> requires (is_variant_derived_v<Variant>)
  friend auto operator->*( Variant && variant, variant_method const& self ) {
    return [&](auto&&...args)->decltype(auto) {
      return std::visit( [&](auto&& val)->decltype(auto) {
        return self.f( decltype(val)(val), decltype(args)(args)... );
      }, get_variant_base(std::forward<Variant>(variant)) );
    };
  }
};

template<class...Ts>
struct variant_ptr:std::variant<Ts...> {
  using base = std::variant<Ts...>;
  using base::base;

  auto& operator*() const { return ((*this)->*dereference)(); }
  auto& operator*() { return ((*this)->*dereference)(); }

  auto* get() const { return std::addressof(**this); }
  auto* get() { return std::addressof(**this); }

  auto* operator->() const { return get(); }
  auto* operator->() { return get(); }

  explicit operator bool() const { return ((*this)->*to_bool)(); }
};

1
我不想听起来不像自己,但这不是有点过头了吗?拥有这些“variant_method”有什么优势呢? - Quentin
@Quentin 为什么要写狭窄的代码,当你可以实现一个20行库的适度语言扩展呢?它们是作用于变量的方法指针,而且非常通用。我的意思是,试着在你的 variant<Ts*...> 中使用一组派生类型来实现多态性;现在尝试 (var->*operator_<Base*>)()。有很多有趣的东西可以得到;虽然所有这些都可以用 visit 来完成,但我发现当你使用和编写变量成员函数指针时,你会以不同的方式思考它们。 - Yakk - Adam Nevraumont
基本上,这是从外部模拟向std::variant添加“passthrough”方法。嗯,为什么不呢。但我并不认为疯狂的调用语法会使它们在没有包装器的情况下可行。编辑:实时重新加载注释编辑将是很棒的 :) - Quentin
@Quentin 这是标准成员函数指针调用语法。你可以很容易地使 var->*mem_ptr(args...) 工作,但操作顺序使得它不能与“真正”的成员函数一起工作,所以我有两种想法。哦,还有一个有趣的小细节,你可以将 variant_method 修改为 any_method 并在扩展的 std::any 上支持它,但这会变得非常冗长(而且这是以上过度的1个单位的度量)。 - Yakk - Adam Nevraumont
是的,它并不是那么疯狂,只是有点笨重。每次我不得不在 ->* 周围加括号并处理其奇怪的优先级时,我个人都会叹气。 - Quentin

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