地图、集合等的array_view替代方案

array_view alternative for maps, sets, etc

本文关键字:view 方案 array 集合 地图      更新时间:2023-10-16

假设我有一个类层次结构,它有几个返回容器引用的virtual函数:

#include <vector>
#include <set>
#include <map>
#include <unordered_set>
#include <unordered_map>
class Interface {
public:
    virtual const std::vector<int>& getArray() const = 0;
    virtual const std::set<int>& getSet() const = 0;
    virtual const std::map<int, int>& getMap() const = 0;
};
class SubclassA : public Interface {
public:
    const std::vector<int>& getArray() const override { return _vector; }
    const std::set<int>& getSet() const override { return _set; }
    const std::map<int, int>& getMap() const override { return _map; }
private:
    std::vector<int> _vector;
    std::set<int> _set;
    std::map<int, int> _map;
};

目前,只能在Interface类的任何子类中实际返回vectorsetmap。然而,对于vector部分,我可以使用例如gsl::array_view来软化这种限制:

class Interface {
public:
    virtual gsl::array_view<const int> getArray() const = 0;
    virtual const std::set<int>& getSet() const = 0;
    virtual const std::map<int, int>& getMap() const = 0;
};
class SubclassA : public Interface {
public:
    gsl::array_view<const int> getArray() const override { return _vector; }
    const std::set<int>& getSet() const override { return _set; }
    const std::map<int, int>& getMap() const override { return _map; }
private:
    std::vector<int> _vector;
    std::set<int> _set;
    std::map<int, int> _map;
};
class SubclassB : public Interface {
public:
   gsl::array_view<const int> getArray() const override { return _array; }
//    const std::set<int>& getSet() const override { return _set; }
//    const std::map<int, int>& getMap() const { return _map; }
private:
    std::array<int, 3> _array;
    std::unordered_set<int> _set;
    std::unordered_map<int, int> _map;
};

所以问题是,有没有array_view的替代品可以与其他容器类型一起使用?基本上,我想要的只是一个轻量级对象,我可以从一个函数返回该对象,该函数将充当某个容器的不可变视图,而无需指定特定的容器类型。对我来说,把std::set推到类似array_view的东西上甚至是有意义的,但支持的操作更少(例如,没有随机访问)。map显然是不同的野兽,需要不同的支持关联查找的view,但即使对于map,我认为有能力说出array_view<const std::pair<const int, int>>也是有用的。我是不是要求太多了?或者也许有合理的方法来实现这一点?或者甚至可能存在这种"视图"的现有实现?

附言:继承不是先决条件——我只是认为这是解决问题的最简单方法。

如果您只是在寻找一个类型擦除的范围,您可以查看boost::any_range:

using IntRange = boost::any_range<
                     int,
                     boost::forward_traversal_tag,
                     int,
                     std::ptrdiff_t>;
int sum(IntRange const& range) {
    return std::accumulate(range.begin(), range.end(), 0);
}
int main()
{
    std::cout << sum(std::vector<int>{1, 2, 3}) << std::endl;  // OK, 6
    std::cout << sum(std::set<int>{4, 5, 6}) << std::endl;     // OK, 15
}

即使你试图滥用它:

sum(std::map<int, int>{})

错误信息并不可怕:

/usr/local/include/boost/range/detail/any_iterator_wrapper.hpp:40:60: error: invalid static_cast from type 'std::pair<const int, int>' to type 'int&'
             return static_cast<Reference>(const_cast<T&>(x));
                                                            ^

您可以为您的用例创建一个别名:

template <typename T>
using FwdImmutableRangeT = boost::any_range<T,
                               boost::forward_traversal_tag,
                               const T&, std::ptrdiff_t>;

并返回这些:

class Interface {
public:
    virtual FwdImmutableRange<int> getArray() const = 0;
    virtual FwdImmutableRange<const int> getSet() const = 0;
    virtual FwdImmutableRange<std::pair<const int, int>> getMap() const = 0;
};

我不知道有一个,但一组给定接口的通用视图并不难写。您可以将类型擦除与手动vtable结合使用,以保持事物的自由分配。

存储一个pvoid和一个指向函数表的指针。每个函数都会擦除一个操作的类型依赖关系。

如果操作具有签名R(Args...),则表中的擦除函数指针具有签名R(void*, Args...)。我使用无状态lambda在vtable工厂中编写它们,该工厂构造一个静态本地vtable并返回一个指向常量vtable的指针。

面向用户的类公开操作,并将它们转发给vtable。它有一个模板ctor,它将pvoid存储到传递的值,并从工厂获取特定类型的vtable。

您必须小心复制/移动视图类的ctor:模板ctor应防止SFINAE接受视图类实例。

令人讨厌的是,您要么必须为关联容器的工作方式定义新的语义,要么还需要键入擦除它们的迭代器。在视图中,因为迭代器被认为是类值的。这是矢量的一大优势,因为可以使用T*

现在我想起来了,boost有类型擦除迭代器,可能还有关联容器的视图。

如果我们只想要"它在那里吗"功能(以及地图的"它是什么"),而不需要迭代,这很容易:

namespace details {
  template<class K>
  using exists = bool(*)(void*, K const&);
  template<class K, class V>
  using get = V(*)(void*, K const&);
  template<class T>
  struct setlike_vtable {
    exists<T> pexists = 0;
    template<class S>
    static void ctor( setlike_vtable* table ) {
      table->pexists = [](void* p, T const& k)->bool {
        S*ps = static_cast<S*>(p);
        return ps->find(k) != ps->end();
      };
    }
    template<class S>
    static setlike_vtable const* make() {
      static const setlike_vtable retval = []{
        setlike_vtable retval;
        ctor<S>(&retval);
        return retval;
      }();
      return &retval;
    }
  };
  template<class K, class V>
  struct maplike_vtable : setlike_vtable<K> {
    get<K,V> pget = 0;
    template<class S>
    static void ctor( maplike_vtable* table ) {
      setlike_vtable<K>::template ctor<S>(table);
      table->pget = [](void* p, K const& k)->V {
        S*ps = static_cast<S*>(p);
        return ps->find(k)->second;
      };
    }
    template<class S>
    static maplike_vtable const* make() {
      static const maplike_vtable retval = []{
        maplike_vtable retval;
        ctor<S>(&retval);
        return retval;
      }();
      return &retval;
    }
  };
}
template<class T>
struct set_view {
  details::setlike_vtable<T> const* vtable = 0;
  void* pvoid = 0;
  template<class U,
    std::enable_if_t<!std::is_same<std::decay_t<U>, set_view>{}, int> =0
  >
  set_view(U&& u):
    vtable( details::setlike_vtable<T>::template make<std::decay_t<U>>() ),
    pvoid( const_cast<void*>( static_cast<void const*>( std::addressof(u) ) ) )
  {}
  set_view(set_view const&)=default;
  set_view() = default;
  ~set_view() = default;
  set_view& operator=(set_view const&)=delete;
  explicit operator bool() const { return vtable; }
  bool exists( T const&t ) const {
    return vtable->pexists( pvoid, t );
  }
};
template<class K, class V>
struct map_view {
  details::maplike_vtable<K, V> const* vtable = 0;
  void* pvoid = 0;
  template<class U,
    std::enable_if_t<!std::is_same<std::decay_t<U>, map_view>{}, int> =0
  >
  map_view(U&& u):
    vtable( details::maplike_vtable<K,V>::template make<std::decay_t<U>>() ),
    pvoid( const_cast<void*>( static_cast<void const*>( std::addressof(u) ) ) )
  {}
  map_view(map_view const&)=default;
  map_view() = default;
  ~map_view() = default;
  map_view& operator=(map_view const&)=delete;
  explicit operator bool() const { return vtable; }
  bool exists( K const&k ) const {
    return vtable->pexists( pvoid, k );
  }
  V get( K const& k ) const {
    return vtable->pget( pvoid, k );
  }
};

请注意,如果不希望get按值返回,则通常需要map_view< Key, Value const& >

活生生的例子。

通过访问进行迭代很容易,但需要对传入的访问者进行类型擦除(比如std::function)。通过迭代器进行迭代需要类型擦除的迭代器,而类型擦除的迭代器必须具有值语义。在这一点上,您最好窃取boost的实现。

现在提出的推论提供了解决问题的另一种方法;让类型被擦除的视图使用协程来实现枚举,而不是访问。

我敢打赌,上面的视图比boost::any_range稍快,因为由于设计原因,它需要做的工作较少。您可以通过将vtable移动到视图主体中的内联位置来加快速度,消除缓存未命中;对于较大类型的擦除,这可能会导致运行时内存膨胀,但上述类型的擦除视图在vtable中存储1-2个指针。将一个指针指向1-2个指针似乎很愚蠢。

相关文章: