C++ folly库解读(二) small_vector —— 小数据集下的std::vector替代方案

介绍

行为与std::vector类似,但是使用了small buffer optimization(类似于fbstring中的SSO),将指定个数的数据内联在对象中,而不是像std::vector直接将对象分配在堆上,避免了malloc/free的开销。

small_vector基本兼容std::vector的接口。

small_vector<int,2> vec;

vec.push_back(0);     // Stored in-place on stack
vec.push_back(1);     // Still on the stack
vec.push_back(2);    // Switches to heap buffer.

small_vector<int,2> vec指定可以内联在对象中2个数据:

image

当超过2个后,后续添加的数据会被分配到堆上,之前的2个数据也会被一起move到堆上:

image

使用场景

根据官方文档的介绍,small_vector在下面3种场景中很有用:

  • 需要使用的vector的生命周期很短(比如在函数中使用),并且存放的数据占用空间较小,那么此时节省一次malloc是值得的。
  • 如果vector大小固定且需要频繁查询,那么在绝大多数情况下会减少一次cpu cache miss,因为数据是内联在对象中的。
  • 如果需要创建上亿个vector,而且不想在记录capacity上浪费对象空间(一般的vector对象内会有三个字段:pointer _Myfirst、pointer _Mylast、pointer _Myend)。small_vector允许让malloc来追踪allocation capacity(这会显著的降低insertion/reallocation效率,如果对这两个操作的效率比较在意,你应该使用FBVector,FBVector在官方描述中可以完全代替std::vector)

比如在io/async/AsyncSocket.h中,根据条件的不同使用small_vector或者std::vector:

  // Lifecycle observers.
  //
  // Use small_vector to avoid heap allocation for up to two observers, unless
  // mobile, in which case we fallback to std::vector to prioritize code size.
using LifecycleObserverVecImpl = conditional_t<
      !kIsMobile,
      folly::small_vector<AsyncTransport::LifecycleObserver*, 2>,
      std::vector<AsyncTransport::LifecycleObserver*>>;

LifecycleObserverVecImpl lifecycleObservers_;


// push_back
void AsyncSocket::addLifecycleObserver(
    AsyncTransport::LifecycleObserver* observer) {
  lifecycleObservers_.push_back(observer);
}

// for loop
for (const auto& cb : lifecycleObservers_) {
    cb->connect(this);
}

// iterator / erase
 const auto eraseIt = std::remove(
      lifecycleObservers_.begin(), lifecycleObservers_.end(), observer);
  if (eraseIt == lifecycleObservers_.end()) {
    return false;
}

为什么不是std::array

下面两种情况,small_vector比std::array更合适:

  • 需要空间后续动态增长,不仅仅是编译期的固定size。
  • 像上面的例子,根据不同条件使用std::vector/small_vector,且使用的API接口是统一的。

其他用法

  • NoHeap : 当vector中数据个数超过指定个数时,不会再使用堆。如果个数超过指定个数,会抛出std::length_error异常。
  • <Any integral type> : 指定small_vector中size和capacity的数据类型。
// With space for 32 in situ unique pointers, and only using a 4-byte size_type.
small_vector<std::unique_ptr<int>, 32, uint32_t> v;

// A inline vector of up to 256 ints which will not use the heap.
small_vector<int, 256, NoHeap> v;

// Same as the above, but making the size_type smaller too.
small_vector<int, 256, NoHeap, uint16_t> v;

其中,依赖boost::mpl元编程库,可以让后两个模板变量任意排序。

其他类似库

Benchmark

没有找到官方的benchmark,自己简单的写了一个,不测试数据溢出到堆上的情况。

插入4个int,std::vector使用reserve(4)预留空间。

BENCHMARK(stdVector, n) {
  FOR_EACH_RANGE(i, 0, n) {
    std::vector<int> vec;
    vec.reserve(4);

    for (int i = 0; i < 4; i++) {
      vec.push_back(1);
    }

    doNotOptimizeAway(vec);
  }
}

BENCHMARK_DRAW_LINE();

BENCHMARK_RELATIVE(smallVector, n) {
  FOR_EACH_RANGE(i, 0, n) {
    small_vector<int, 4> vec;

    for (int i = 0; i < 4; i++) {
      vec.push_back(1);
    }

    doNotOptimizeAway(vec);
  }
}

结果是:small_vector比std::vector快了40%:

============================================================================
delve_folly/benchmark.cc                        relative  time/iter  iters/s
============================================================================
stdVector                                                  440.79ns    2.27M
----------------------------------------------------------------------------
smallVector                                      140.48%   313.77ns    3.19M
============================================================================

如果把stdVector中的vec.reserve(4);去掉,那么small_vector速度比std::vector快了3倍。在我的环境上,std::vector的扩容因子为2,如果不加reserve,那么std::vector会有2次扩容的过程(貌似很多人不习惯加reserve,是有什么特别的原因吗 : )):

============================================================================
delve_folly/benchmark.cc                        relative  time/iter  iters/s
============================================================================
stdVector                                                    1.26us  795.06K
----------------------------------------------------------------------------
smallVector                                      417.25%   301.44ns    3.32M
============================================================================

代码关注点

small_vector代码比较少,大概1300多行。主要关注以下几个方面:

  • 主要类。
  • 数据存储结构,capacity的存储会复杂一些。
  • 扩容过程,包括数据从对象中迁移到堆上。
  • 常用的函数,例如push_back、reserve、resize。
  • 使用makeGuard代替了原版的try-catch。
  • 通过boost::mpl支持模板参数任意排序。
  • 通过boost-operators简化operator以及C++20的<=>。

主要类

image

  • small_vector : 包含的核心字段为union Datastruct HeapPtrstruct HeapPtrWithCapacity,这三个字段负责数据的存储。此外small_vector对外暴露API接口,例如push_back、reserve、resize等。
  • small_vector_base : 没有对外提供任何函数接口,类内做的就是配合boost::mpl元编程库在编译期解析模板参数,同时生成boost::totally_ordered1供small_vector继承,精简operator代码。
  • IntegralSizePolicyBase:负责size/extern/heapifiedCapacity相关的操作。
  • IntegralSizePolicy : 负责内联数据溢出到堆的过程。

small_vector

声明:

template <
    class Value,
    std::size_t RequestedMaxInline = 1,
    class PolicyA = void,
    class PolicyB = void,
    class PolicyC = void>
class small_vector : public detail::small_vector_base<
                         Value,
                         RequestedMaxInline,
                         PolicyA,
                         PolicyB,
                         PolicyC>::type 

声明中有三个策略模板参数是因为在一次提交中删除了一个无用的策略,OneBitMutex:Delete small_vector's OneBitMutex policy

small_vector_base

template <
    class Value,
    std::size_t RequestedMaxInline,
    class InPolicyA,
    class InPolicyB,
    class InPolicyC>
struct small_vector_base;

boost::mpl放到最后说吧 :)

数据结构

small_vector花了一些心思在capacity的设计上,尽可能减小对象内存,降低内联数据带来的影响。

union Data负责存储数据:

union Data{
    PointerType pdata_;          // 溢出到堆后的数据
    InlineStorageType storage_;  // 内联数据
}u;

InlineStorageType

使用std::aligned_storage进行初始化,占用空间是sizeof(value_type) * MaxInline,对齐要求为alignof(value_type)

typedef typename std::aligned_storage<sizeof(value_type) * MaxInline, alignof(value_type)>::type InlineStorageType;

capacity

与std::vector用结构体字段表示capacity不同,small_vector的capacity存放分为三种情况。

capacity内联在对象中

这是最简单的一种情况:

image

条件为sizeof(HeapPtrWithCapacity) < sizeof(InlineStorageType) (这里我不明白为什么等于的情况不算在内):

static bool constexpr kHasInlineCapacity = sizeof(HeapPtrWithCapacity) < sizeof(InlineStorageType);

typedef typename std::conditional<kHasInlineCapacity, HeapPtrWithCapacity, HeapPtr>::type PointerType;

struct HeapPtrWithCapacity {
  value_type* heap_;
  InternalSizeType capacity_;
} ;

union Data{
    PointerType pdata_;           // 溢出到堆后的数据
    InlineStorageType storage_;   // 内联数据
}u;

通过malloc_usable_size获取capacity

假如上述kHasInlineCapacity == false,即sizeof(InlineStorageType) <= sizeof(HeapPtrWithCapacity)时,考虑到节省对象空间,capacity不会内联在对象中,此时PointerType的类型为HeapPtr,内部只保留一个指针:

struct HeapPtr {
  value_type* heap_;
} ;

union Data{
    PointerType pdata_;           // 溢出到堆后的数据
    InlineStorageType storage_;   // 内联数据
}u;

那么此时capacity存放在哪里了呢?这里又分了两种情况,第一种就是这里要说明的:直接通过malloc_usable_size获取从堆上分配的内存区域的可用数据大小,这个结果就被当做small_vector当前的capacity:

malloc_usable_size(heap_) / sizeof(value_type);    // heap_指向堆上的数据

image

但是有一个问题,由于内存分配存在alignment和minimum size constraints的情况,malloc_usable_size返回的大小可能会大于申请时指定的大小,但是folly会利用这部分多余的空间来存放数据(如果能放的下)。

比如在不使用jemalloc的情况下,在扩容的函数内,将向系统申请的字节数、malloc_usable_size返回的可用空间、small_vector的capacity打印出来:

folly::small_vector<uint32_t, 2> vec;     // uint32_t => four bytes

for (int i = 0; i < 200; i++) {
    vec.push_back(1);
    std::cout << vec.capacity() << std::endl;
}

// 代码进行了简化
template <typename EmplaceFunc>
  void makeSizeInternal(size_type newSize, bool insert, EmplaceFunc&& emplaceFunc, size_type pos) {
    const auto needBytes = newSize * sizeof(value_type);
    const size_t goodAllocationSizeBytes = goodMallocSize(needBytes);
    const size_t newCapacity = goodAllocationSizeBytes / sizeof(value_type);

    const size_t sizeBytes = newCapacity * sizeof(value_type);
    void* newh = checkedMalloc(sizeBytes);

    std::cout << "sizeBytes:" << sizeBytes << " malloc_usable_size:" << malloc_usable_size(newh) << " "
              << kMustTrackHeapifiedCapacity << std::endl;

    // move元素等操作,略过....
}

// output :
2
2
sizeBytes:16 malloc_usable_size:24 0
6
6
6
6
sizeBytes:40 malloc_usable_size:40 0
10
10
10
10
sizeBytes:64 malloc_usable_size:72 0
18
18
18
18
18
18
18
18
......
......

可以看出,扩容时即使向系统申请16字节的空间,malloc_usable_size返回的是24字节,而small_vector此时的capacity也是24,即会利用多余的8个字节额外写入2个数据。

**如果使用了jemalloc **,那么会根据size classes分配空间。

这种方式也是有使用条件的,即needbytes >= kHeapifyCapacityThreshold,kHeapifyCapacityThreshold的定义为:

// This value should we multiple of word size.
static size_t constexpr kHeapifyCapacitySize = sizeof(typename std::aligned_storage<sizeof(InternalSizeType), alignof(value_type)>::type);

// Threshold to control capacity heapifying.
static size_t constexpr kHeapifyCapacityThreshold = 100 * kHeapifyCapacitySize;

我没想明白这个100是怎么定下来的 😦

将capacity放到堆上

当需要申请的内存needbytes >= kHeapifyCapacityThreshold时,就会直接将capacity放到堆上进行管理:

image

此时需要多申请sizeof(InternalSizeType)字节来存放capacity,并且需要对内存分配接口返回的指针加上sizeof(InternalSizeType)从而指向真正的数据:

inline void* shiftPointer(void* p, size_t sizeBytes) { 
    return static_cast<char*>(p) + sizeBytes; 
}

template <typename EmplaceFunc>
void makeSizeInternal(size_type newSize, bool insert, EmplaceFunc&& emplaceFunc, size_type pos) {
   ....
    const bool heapifyCapacity = !kHasInlineCapacity && needBytes >= kHeapifyCapacityThreshold;

    // 申请内存
    void* newh = checkedMalloc(sizeBytes);
    value_type* newp =
        static_cast<value_type*>(heapifyCapacity ? detail::shiftPointer(newh, kHeapifyCapacitySize) : newh);
    
    // move元素等操作,略过....
    u.pdata_.heap_ = newp;
   // ....
}

size()相关

那么如何区分数据是内联在对象中还是溢出到堆上,如何区分上面三种capacity存储策略呢?

采取的做法是向size借用两位来区分:

image

  • kExternMask : 数据是否溢出到堆上,相关函数为isExtern/setExtern.
  • kCapacityMask : capacity是否在堆上额外分配了内存来管理。相关函数为isHeapifiedCapacity/setHeapifiedCapacity.
template <class SizeType, bool ShouldUseHeap>
struct IntegralSizePolicyBase {
  IntegralSizePolicyBase() : size_(0) {}
 protected:
  static constexpr std::size_t policyMaxSize() { return SizeType(~kClearMask); }

  std::size_t doSize() const { return size_ & ~kClearMask; }

  std::size_t isExtern() const { return kExternMask & size_; }

  void setExtern(bool b) {
    if (b) {
      size_ |= kExternMask;
    } else {
      size_ &= ~kExternMask;
    }
  }

  std::size_t isHeapifiedCapacity() const { return kCapacityMask & size_; }

  void setHeapifiedCapacity(bool b) {
    if (b) {
      size_ |= kCapacityMask;
    } else {
      size_ &= ~kCapacityMask;
    }
  }
  void setSize(std::size_t sz) {
    assert(sz <= policyMaxSize());
    size_ = (kClearMask & size_) | SizeType(sz);
  }

 private:
  // We reserve two most significant bits of size_.
  static SizeType constexpr kExternMask =
      kShouldUseHeap ? SizeType(1) << (sizeof(SizeType) * 8 - 1) : 0;

  static SizeType constexpr kCapacityMask =
      kShouldUseHeap ? SizeType(1) << (sizeof(SizeType) * 8 - 2) : 0;

  SizeType size_;
};

都是很简单的位运算。只需要注意下policyMaxSize函数,因为向size借了2位,所以最大的size不是SizeType类型的最大值,需要有额外的判断。

capacity()函数

因为capacity有三种存储方式,所以需要根据各自情况去获取:

size_type capacity() const {
  if (this->isExtern()) {
    if (hasCapacity()) {       // 为capacity分配内存的情况
      return u.getCapacity();
    }
    return AllocationSize{}(u.pdata_.heap_) / sizeof(value_type);   // 不为capacity分配空间的情况
  }
  return MaxInline;    // 数据内联的情况
}

数据内联在对象中

这是最简单的情况,根据上面说过的isExtern()判断数据是否内联,是的话直接返回MaxInline。

这里的MaxInline还不是上游传过来的RequestedMaxInline。因为不管是什么capacity存储策略,union Data必然会有一个有指针,最小也是sizeof(void*),假如用户传的是small_vector<uint8_t,1>,会替用户修改MaxInine = 8,最大限度利用对象空间:

/*
  * Figure out the max number of elements we should inline.  (If
  * the user asks for less inlined elements than we can fit unioned
  * into our value_type*, we will inline more than they asked.)
  */
static constexpr std::size_t MaxInline{
    constexpr_max(sizeof(Value*) / sizeof(Value), RequestedMaxInline)};

为capacity分配了内存

这里包括capacity分配的内存在堆上或者内联在对象中。通过hasCapacity()判断,isHeapifiedCapacity上面说过:

bool hasCapacity() const {
  return kHasInlineCapacity || this->isHeapifiedCapacity();
}

如果hasCapacity()为true,则调用u.getCapacity(),可以猜到这个方法调用PointerType(HeapPtr/HeapPtrWithCapacity)对应的getCapacity()方法。

union Data {
    PointerType pdata_;
    InlineStorageType storage_;

    InternalSizeType getCapacity() const { return pdata_.getCapacity(); }
  } u;
};

inline void* unshiftPointer(void* p, size_t sizeBytes) {
  return static_cast<char*>(p) - sizeBytes;
}

struct HeapPtr {
  // heap[-kHeapifyCapacitySize] contains capacity
  value_type* heap_;

  InternalSizeType getCapacity() const {
    return *static_cast<InternalSizeType*>(
        detail::unshiftPointer(heap_, kHeapifyCapacitySize));
  }
};

struct HeapPtrWithCapacity {
  value_type* heap_;
  InternalSizeType capacity_;

  InternalSizeType getCapacity() const { return capacity_; }
};

注意unshiftPointer是shiftPointer的反过程,将指针从指向真正的数据回退到指向capacity。

不为capacity分配空间

即通过malloc_usable_size获取capacity。AllocationSize{}(u.pdata_.heap_)/sizeof(value_type);直接理解为malloc_usable_size(u.pdata_.heap_)/sizeof(value_type)即可,加AllocationSize是为了解决一个Android not having malloc_usable_size below API17,不是重点。

size_type capacity() const {
  if (this->isExtern()) {
    if (hasCapacity()) {
      return u.getCapacity();
    }
    return AllocationSize{}(u.pdata_.heap_) / sizeof(value_type);   // 此种场景
  }
  return MaxInline;
}

扩容

与std::vector扩容的不同点:

  • small_vector根据模板参数是否满足is_trivially_copyable进行了不同的实现:
  • std::vector迁移元素时,会根据是否有noexcept move constructor来决定调用move constructor还是copy constructor(之前这篇文章提到过:c++ 从vector扩容看noexcept应用场景)。但small_vector没有这个过程,有move constructor就直接调用,不判断是否有noexcept。所以,当调用move constructor有异常时,原有内存区域的数据会被破坏
  • 扩容因子不同。std::vector一般为2或者1.5,不同平台不一样。small_vector的capacity上面已经提到过。

最终扩容流程都会走到moveToUninitialized函数。

/*
  * Move a range to a range of uninitialized memory.  Assumes the
  * ranges don't overlap.
  */
template <class T>
typename std::enable_if<!folly::is_trivially_copyable<T>::value>::type moveToUninitialized(T* first, T* last, T* out) {
  std::size_t idx = 0;
  {
    auto rollback = makeGuard([&] {
      for (std::size_t i = 0; i < idx; ++i) {
        out[i].~T();
      }
    });
    for (; first != last; ++first, ++idx) {
      new (&out[idx]) T(std::move(*first));
    }
    rollback.dismiss();
  }
}

// Specialization for trivially copyable types.
template <class T>
typename std::enable_if<folly::is_trivially_copyable<T>::value>::type moveToUninitialized(T* first, T* last, T* out) {
  std::memmove(static_cast<void*>(out), static_cast<void const*>(first), (last - first) * sizeof *first);
}

这里我不太理解的一点是,既然注释中提到假设前后内存没有overlap,那么在is_trivially_copyable的版本中为什么不用std::memcpy呢?效率还能高一点。

先迁移原有数据还是先放入新数据

在之前的版本中,流程是申请新内存、迁移原有数据、放入新数据:

template<class ...Args>
void emplaceBack(Args&&... args) {
  makeSize(size() + 1);        // 申请新内存 + 迁移原有数据,内部会调用上面的moveToUninitialized
  new (end()) value_type(std::forward<Args>(args)...);    //放入新数据
  this->setSize(size() + 1);
}

small_vector improvements提交中,改为了申请新内存、放入新数据、迁移原有数据。理由是有可能emplace_back的数据是small_vector中的某一个数据的引用, 比如这次提交中加的ForwardingEmplaceInsideVector test,不过这属于corner case。

makeGuard

Support -fno-exceptions in folly/small_vector.h提交里,使用makeGuard代替了原有的try-catch。makeGuard定义在folly/ScopeGuard.h中。

之前try-catch版本:

template <class T>
typename std::enable_if<!folly::is_trivially_copyable<T>::value>::type
moveToUninitialized(T* first, T* last, T* out) {
  std::size_t idx = 0;
  try {
    for (; first != last; ++first, ++idx) {
      new (&out[idx]) T(std::move(*first));
    }
  } catch (...) {
    for (std::size_t i = 0; i < idx; ++i) {
      out[i].~T();
    }
    throw;
  }
}

可以对比上面的makeGuard版本,逻辑没有变化。

Andrei Alexandrescu和Petru Marginean在2000年写的Generic: Change the Way You Write Exception-Safe Code — Forever中,介绍了编写Exception-Safe Code常见的方法并提出了ScopeGuard的概念:

  • 什么都不加,战略上藐视敌人,战术上也藐视敌人。
  • try-catch : 最常见,缺点是:异常路径性能损失较大(stack-unwinding)、编译后的二进制文件体积膨胀等。
  • RAII : 需要针对每一种异常写一个小类去处理,繁琐。
  • scopeGuard : 上面提到的用法,也是利用了RAII的思想,但是更加通用。应用程序只需要定义rollback,并在成功后调用dismiss即可。

其他

clear优化

optimize small_vector::clear提交中,利用了Clang/GCC对clear函数进行了优化,生成更少的汇编指令(这都是怎么发现的???):

// 优化前
void clear() {
  erase(begin(), end());
}

// 优化后
void clear() {
  // Equivalent to erase(begin(), end()), but neither Clang or GCC are able to optimize away the abstraction.
    for (auto it = begin(); it != end(); ++it) {
      it->~value_type();
    }
    this->setSize(0);
}

image

__attribute__((__pack__))/pragma(pack(push, x))

folly中包装了编译器对齐系数,相关参数介绍 : C/C++内存对齐详解

// packing is very ugly in msvc
#ifdef _MSC_VER
#define FOLLY_PACK_ATTR /**/
#define FOLLY_PACK_PUSH __pragma(pack(push, 1))
#define FOLLY_PACK_POP __pragma(pack(pop))
#elif defined(__GNUC__)
#define FOLLY_PACK_ATTR __attribute__((__packed__))
#define FOLLY_PACK_PUSH /**/
#define FOLLY_PACK_POP /**/
#else
#define FOLLY_PACK_ATTR /**/
#define FOLLY_PACK_PUSH /**/
#define FOLLY_PACK_POP /**/
#endif

Fix alignment issues in small_vector提交中,small_vector改为了只在64位平台使用对齐系数(不知道为什么,哭。。。):

#if (FOLLY_X64 || FOLLY_PPC64)
#define FOLLY_SV_PACK_ATTR FOLLY_PACK_ATTR
#define FOLLY_SV_PACK_PUSH FOLLY_PACK_PUSH
#define FOLLY_SV_PACK_POP FOLLY_PACK_POP
#else
#define FOLLY_SV_PACK_ATTR
#define FOLLY_SV_PACK_PUSH
#define FOLLY_SV_PACK_POP
#endif

boost::mpl元编程库

boost mpl文档

small_vector_base的代码很少,关注下boost::mpl的使用:

namespace mpl = boost::mpl;

template <
    class Value,
    std::size_t RequestedMaxInline,
    class InPolicyA,
    class InPolicyB,
    class InPolicyC>
struct small_vector_base {

  typedef mpl::vector<InPolicyA, InPolicyB, InPolicyC> PolicyList;

  /*
   * Determine the size type
   */
  typedef typename mpl::filter_view<
      PolicyList,
      std::is_integral<mpl::placeholders::_1>>::type Integrals;
  typedef typename mpl::eval_if<
      mpl::empty<Integrals>,
      mpl::identity<std::size_t>,
      mpl::front<Integrals>>::type SizeType;

  /*
   * Determine whether we should allow spilling to the heap or not.
   */
  typedef typename mpl::count<PolicyList, small_vector_policy::NoHeap>::type
      HasNoHeap;

  /*
   * Make the real policy base classes.
   */
  typedef IntegralSizePolicy<SizeType, !HasNoHeap::value> ActualSizePolicy;

  /*
   * Now inherit from them all.  This is done in such a convoluted
   * way to make sure we get the empty base optimizaton on all these
   * types to keep sizeof(small_vector<>) minimal.
   */
  typedef boost::totally_ordered1<
      small_vector<Value, RequestedMaxInline, InPolicyA, InPolicyB, InPolicyC>,
      ActualSizePolicy>
      type;
};

解析模板参数的思路为:

  • typedef mpl::vector<InPolicyA, InPolicyB, InPolicyC> PolicyList; : 将策略放入到mpl::vector中,例如Noheap、指定size的数据类型(uint32_t、uint16_t等)

接下来可以分为两块,获取SizeType和获取HasNoHeap:

获取SizeType:

  • typedef typename mpl::filter_view<PolicyList, std::is_integral<mpl::placeholders::_1>>::type Integrals; : 将符合std::is_integral条件的筛选出来,比如uint8_t。
    • mpl::filter_view被定义为template< typename Sequence, typename Pred>,其中,Pred需要是Unary Lambda Expression,即为编译期可调用的entity,分为Metafunction ClassPlaceholder Expression,上面的std::is_integral<mpl::placeholders::_1>即为Placeholder Expression
  • typedef typename mpl::eval_if<mpl::empty<Integrals>, mpl::identity<std::size_t>, mpl::front<Integrals>>::type SizeType;不用知道每个函数的意思,从字面也能看出来:假如应用层传入的模板参数没有std::is_integral,那么SizeType,即size的类型,就是size_t,否则就是应用传入的类型,比如uint8_t.

获取HasNoHeap:

  • typedef typename mpl::count<PolicyList, small_vector_policy::NoHeap>::type HasNoHeap;,这个也能猜出来:应用层是否指定了NoHeap.

image

可以看出,解析模板参数并没有依赖参数的特定顺序。

boost/operators

boost/operators文档参考 : Header <boost/operators.hpp>

small_vector_base的最后两行代码,与boost/operators有关 :

typedef IntegralSizePolicy<SizeType, !HasNoHeap::value> ActualSizePolicy;

typedef boost::totally_ordered1<
    small_vector<Value, RequestedMaxInline, InPolicyA, InPolicyB, InPolicyC>,
    ActualSizePolicy>
    type;

boost/operators为精简operator代码而设计,比如我们想要支持x < y,那么x > y、x >= y、和x <= y同样也需要。但是理想情况下,我们只需要重载operator<就行了,后面三个操作可以根据x<y推导出来,boost/operators可以为我们生成类似下面的代码:

bool operator>(const T& x, const T& y)  { return y < x; }
bool operator<=(const T& x, const T& y) { return !static_cast<bool>(y < x); }
bool operator>=(const T& x, const T& y) { return !static_cast<bool>(x < y); }

boost::totally_ordered1被small_vector继承。按照boost::totally_ordered1的定义,只要实现operator<operator=,那么除了这两个操作,boost/operators也会自动生成operator>operator<=operator>=operator!=

// class small_vector

bool operator==(small_vector const& o) const {
    return size() == o.size() && std::equal(begin(), end(), o.begin());
}

bool operator<(small_vector const& o) const {
    return std::lexicographical_compare(begin(), end(), o.begin(), o.end());
}

对比std::vector,在c++20之前,std::vector实现了所有的operator==operator!=operator<operator<=operator>operator>=,比较繁琐。

c++20的<=> (three-way comparison)

c++20提供了一个新特性,three-way comparison,可以提供上面boost/operators的功能。可以参考:

比如,std::vector在c++20后,就废弃了<、<=、 >=、!= operators (下面引用来自于operator==,!=,<,<=,>,>=,<=>(std::vector)
):

The <, <=, >, >=, and != operators are synthesized from operator<=> and operator== respectively. (since C++20)

(完)

朋友们可以关注下我的公众号,获得最及时的更新:

 

posted @ 2022-11-16 13:40  arthurzyc  阅读(417)  评论(0编辑  收藏  举报