【问题标题】:Using std::variant with recursion, without using boost::recursive_wrapper使用带有递归的 std::variant,而不使用 boost::recursive_wrapper
【发布时间】:2016-09-12 15:56:44
【问题描述】:

我想用 C++17 std::variant 替换 boost::variants 并摆脱 boost::recursive_wrapper,以在以下代码中完全消除对 boost 的依赖。我该怎么做?

#include <boost/variant.hpp>
#include <type_traits>

using v = boost::variant<int, boost::recursive_wrapper<struct s> >;
struct s
{
    v val;
};

template<template <typename...> class R, typename T, typename ... Ts>
auto reduce(T t, Ts ... /*ts*/)
{
    return R<T, Ts...>{t};
}

template<typename T, typename F>
T adapt(F f)
{
    static_assert(std::is_convertible_v<F, T>, "");
    return f;
}

int main()
{
    int  val1 = 42;
    s    val2;
    auto val3 = adapt<v>(reduce<boost::variant>(val1, val2));
}

有两个通用函数:第一个函数reduce 在运行时选择要返回的参数(为了简洁起见,这里它只返回第一个参数),第二个函数adapt 将 F 类型的值转换为 T 类型的值。

在此示例中,reduce 返回一个 boost::variant&lt;int, s&gt; 类型的对象,然后将其转换为 boost::variant&lt;int, boost::recursive_wrapper&lt;s&gt; &gt; 类型的对象。

【问题讨论】:

  • 您可以使用std::variant&lt;int, std::unique_ptr&lt;s&gt;&gt;。但是您需要修改adapt 以将s 映射到unique_ptr&lt;s&gt;
  • 您考虑过使用std::reference_wrapper 吗?

标签: c++ boost c++17


【解决方案1】:

boost::variant 将进行堆分配,以便将自身的一部分递归地定义为自身。 (在其他一些情况下也会堆分配,不确定有多少)

std::variant 不会。 std::variant 拒绝堆分配。

如果没有动态分配,就无法真正拥有包含自身可能变体的结构,因为如果静态声明,这样的结构很容易显示为无限大小。 (您可以通过 N 次不同的递归来对整数 N 进行编码:没有固定大小的缓冲区可以容纳无限量的信息。)

因此,等效的std::variant 存储了一个智能指针,它是自身递归实例的某种占位符。

这可能有效:

struct s;
using v = std::variant< int, std::unique_ptr<s> >;
struct s
{
  v val;
  ~s();
};
inline s::~s() = default;

如果失败,请尝试:

struct destroy_s;
struct s;
using v = std::variant<int, std::unique_ptr<s, destroy_s> >;
struct s
{
  v val;
  ~s();
};
struct destroy_s {
  void operator()(s* ptr){ delete ptr; }
};
inline s::~s() = default;

这确实意味着客户端代码必须有意识地与unique_ptr&lt;s&gt; 交互,而不是直接与struct s 交互。

如果您想支持复制语义,则必须编写一个执行复制的 value_ptr,并为其提供等效于 struct copy_s; 的代码来实现该复制。

template<class T>
struct default_copier {
  // a copier must handle a null T const* in and return null:
  T* operator()(T const* tin)const {
    if (!tin) return nullptr;
    return new T(*tin);
  }
  void operator()(void* dest, T const* tin)const {
    if (!tin) return;
    return new(dest) T(*tin);
  }
};
template<class T, class Copier=default_copier<T>, class Deleter=std::default_delete<T>,
  class Base=std::unique_ptr<T, Deleter>
>
struct value_ptr:Base, private Copier {
  using copier_type=Copier;
  // also typedefs from unique_ptr

  using Base::Base;

  value_ptr( T const& t ):
    Base( std::make_unique<T>(t) ),
    Copier()
  {}
  value_ptr( T && t ):
    Base( std::make_unique<T>(std::move(t)) ),
    Copier()
  {}
  // almost-never-empty:
  value_ptr():
    Base( std::make_unique<T>() ),
    Copier()
  {}

  value_ptr( Base b, Copier c={} ):
    Base(std::move(b)),
    Copier(std::move(c))
  {}

  Copier const& get_copier() const {
    return *this;
  }

  value_ptr clone() const {
    return {
      Base(
        get_copier()(this->get()),
        this->get_deleter()
      ),
      get_copier()
    };
  }
  value_ptr(value_ptr&&)=default;
  value_ptr& operator=(value_ptr&&)=default;

  value_ptr(value_ptr const& o):value_ptr(o.clone()) {}
  value_ptr& operator=(value_ptr const&o) {
    if (o && *this) {
      // if we are both non-null, assign contents:
      **this = *o;
    } else {
      // otherwise, assign a clone (which could itself be null):
      *this = o.clone();
    }
    return *this;
  }
  value_ptr& operator=( T const& t ) {
    if (*this) {
      **this = t;
    } else {
      *this = value_ptr(t);
    }
    return *this;
  }
  value_ptr& operator=( T && t ) {
    if (*this) {
      **this = std::move(t);
    } else {
      *this = value_ptr(std::move(t));
    }
    return *this;
  }
  T& get() { return **this; }
  T const& get() const { return **this; }
  T* get_pointer() {
    if (!*this) return nullptr;
    return std::addressof(get());
  }
  T const* get_pointer() const {
    if (!*this) return nullptr;
    return std::addressof(get());
  }
  // operator-> from unique_ptr
};
template<class T, class...Args>
value_ptr<T> make_value_ptr( Args&&... args ) {
  return {std::make_unique<T>(std::forward<Args>(args)...)};
}

Live example of value_ptr.

【讨论】:

  • 我不太明白为什么你需要clone 当你可以分配时(换句话说 - noisy::operator= 永远不会被调用),但这没关系,因为这不是我的要求。你看,Boost(包括Boost.Variant)都是专家做的,并且有一种现成的做法(递归变体)。现在 std::variant 在没有递归的情况下变得标准化,好像它不是必需的。我想看到的是为什么递归支持不重要的解释,为了更具体,我给出了一个代码示例供您修改(以防止像“使用 unique_ptr,问题已解决”这样的答案)。
  • @sms 这在第 1 和第 2 段中有解释:因为决定 std::variant 不使用堆。第 3 段总结了为什么 std::variant 不能拥有你想要的递归结构,不管你使用什么技巧。 std::variantboost::variant 具有不同的操作语义。除其他外,它几乎永远不会为空,而不是像boost 那样永远​​不会为空。 boost::variant 的代价是它会更频繁地调用 new 的等价物,而 std::variant 是一个将其数据存储在自身内部的标记联合。 value_ptr 提供了解决方案。
  • 我的value_ptr 在存储指针时模拟值语义。实现value_ptr(value_ptr const&amp;) 需要clone。碰巧,它还提供了一个易于编写的operator=。通过在存储值上使用operator= 可以避免分配:我们可以以operator= 合法为条件。我选择了更简单的代码。我将添加一个变体。
  • @Yakk:我不同意你的结论。是的,Boost.Variant 有时会进行堆分配。但这并不是使其变体类型允许递归的原因。这一切都是通过recursive_wrapper 完成的,它实现了您的value_ptr 的有效等效项。标准库可以提供一个类似的 recursive_wrapper 类,而不影响 std::variant 的定义。事实上,您可以毫无问题地使用boost::recursive_wrapperstd::variant
  • @sms: "我想看到的是为什么递归支持不重要的解释" 这不是你的问题问的。您询问了如何在仍然获得递归行为的同时避免使用 Boost。
猜你喜欢
  • 2016-05-10
  • 2012-08-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-02-22
  • 2017-03-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多