(C⁺⁺) Vector

in Android Native

2 minute read

#ifndef ANDROID_VECTOR_H  
#define ANDROID_VECTOR_H  
  
#include <stdint.h>  
#include <sys/types.h>  
  
#include "TypeHelpers.h"  
#include "VectorImpl.h"  
  
namespace android {  
  
template <class TYPE>  
class Vector : private VectorImpl  
{  
    생략  

Android vector도 흔히 사용하는 standard vector와 인터페이스는 크게 다른점이 없다. (그냥 길어서 코드도 생략했다) 템플릿을 쓰기때문에 코드 사이즈를 줄이기 위해, 구체적인 구현은 VectorImpl로 옮겨서 상속을 받는 점 정도를 주목한다.

살펴볼만한 점은 내부에서 데이터를 가지는 방식인데, Android vector는 SharedBuffer라는 녀석을 따로 두어서 그곳을 저장공간으로 삼는다.

namespace android {  
  
class SharedBuffer  
{  
public:  
  
    /* flags to use with release() */  
    enum {  
        eKeepStorage = 0x00000001  
    };  
  
    /*! allocate a buffer of size ‘size’ and acquire() it.  
     *  call release() to free it.  
     */  
    static          SharedBuffer*           alloc(size_t size);  
      
    /*! free the memory associated with the SharedBuffer.  
     * Fails if there are any users associated with this SharedBuffer.  
     * In other words, the buffer must have been release by all its  
     * users.  
     */  
    static          void                    dealloc(const SharedBuffer* released);  
  
    //! access the data for read  
    inline          const void*             data() const;  
      
    //! access the data for read/write  
    inline          void*                   data();  
  
    //! get size of the buffer  
    inline          size_t                  size() const;  
   
    //! get back a SharedBuffer object from its data  
    static  inline  SharedBuffer*           bufferFromData(void* data);  
      
    //! get back a SharedBuffer object from its data  
    static  inline  const SharedBuffer*     bufferFromData(const void* data);  
  
    //! get the size of a SharedBuffer object from its data  
    static  inline  size_t                  sizeFromData(const void* data);  
      
    //! edit the buffer (get a writtable, or non-const, version of it)  
                    SharedBuffer*           edit() const;  
  
    //! edit the buffer, resizing if needed  
                    SharedBuffer*           editResize(size_t size) const;  
  
    //! like edit() but fails if a copy is required  
                    SharedBuffer*           attemptEdit() const;  
      
    //! resize and edit the buffer, loose it’s content.  
                    SharedBuffer*           reset(size_t size) const;  
  
    //! acquire/release a reference on this buffer  
                    void                    acquire() const;  
                      
    /*! release a reference on this buffer, with the option of not  
     * freeing the memory associated with it if it was the last reference  
     * returns the previous reference count  
     */       
                    int32_t                 release(uint32_t flags = 0) const;  
      
    //! returns wether or not we’re the only owner  
    inline          bool                    onlyOwner() const;  
      
  
private:  
        inline SharedBuffer() { }  
        inline ~SharedBuffer() { }  
        SharedBuffer(const SharedBuffer&);  
        SharedBuffer& operator = (const SharedBuffer&);  
   
        // Must be sized to preserve correct alignment.  
        mutable std::atomic<int32_t>        mRefs;                // 레퍼런스 카운터!  
                size_t                      mSize;  
                uint32_t                    mReserved;  
public:  
        // mClientMetadata is reserved for client use.  It is initialized to 0  
        // and the clients can do whatever they want with it.  Note that this is  
        // placed last so that it is adjcent to the buffer allocated.  
                uint32_t                    mClientMetadata;  
};  

이것은 Copy on Write라는 방식을 사용하기 위해서인데, Lazy copy라고도 부른다.

Vector<int> v1;  
Vector<int> v2 = v1;  

과 같은 대입연산이 이루어질때 v1과 v2는 같은 ShareBuffer를 가리키게 되고 SharedBuffer의 RefCount만 증가하는 것이다. 복사는 당장에 일어나지 않는다. (생각해보면 vector의 복사는 굉장히 비용이 많이 든다는걸 알 수 있다)

그러다가 ShareBuffer의 내용이 변할때(즉, v1이나 v2에서 내용을 수정할때) 그때서야 ShareBuffer가 Copy되면서 vector의 컨테이너가 두 개로 나뉜다. 변화가 없다면 굳이 복사에 드는 오버헤드를 지불할 필요가 없다는 생각인 것이다. 괜찮은 생각이다. 아니 훌륭한 생각이다.

VectorImpl을 보면 modern c++ 문법인 trivial과 관련된 값들이 보이는데, Vector는 템플릿으로 구성되어 primitive 외에도 객체를 받을 수 있기에 이런 내용이 존재한다.

namespace android {  
  
class VectorImpl  
{  
public:  
    enum { // flags passed to the ctor  
        HAS_TRIVIAL_CTOR    = 0x00000001,  
        HAS_TRIVIAL_DTOR    = 0x00000002,  
        HAS_TRIVIAL_COPY    = 0x00000004,  
    };  

trivial하다는 것은 단어의 뜻처럼 객체의 복사생성자에서 특별히 아무일도 하지 않음을 의미한다.

SharedBufferoperation new로 공간만 할당되어 있기 때문에 필요한 경우 placement new를 통해 생성자를 불러주어야 제대로 동작한다. 템플릿 인자를 통해 들어온 타입을 type_traits<TYPE>::has_trivial_copyhas_trivial_dtor 등을 통해, 자명한(trivial) 복사생성자가 있는지를 파악하여 Android vector는 그것을 처리한다.

그리고 마지막으로 Android vector에는 Sorted Vector, KeyedVector라는 녀석들도 존재하는데 이것은 heap이나 map같은 녀석이라고 보면 될거같다.