簡體   English   中英

可變大小的結構 C++

[英]Variable Sized Struct C++

這是在 C++ 中制作可變大小結構的最佳方法嗎? 我不想使用向量,因為初始化后長度不會改變。

struct Packet
{
    unsigned int bytelength;
    unsigned int data[];
};

Packet* CreatePacket(unsigned int length)
{
    Packet *output = (Packet*) malloc((length+1)*sizeof(unsigned int));
    output->bytelength = length;
    return output;
}

編輯:重命名變量名並更改代碼以使其更正確。

關於你在做什么的一些想法:

  • 使用 C 風格的可變長度 struct idiom 允許您為每個數據包執行一次免費存儲分配,這是如果struct Packet包含std::vector所需數量的一半。 如果您分配一個非常大的數量的數據包,然后進行一半的自由存儲分配/釋放操作很可能是顯著。 如果您還進行網絡訪問,那么等待網絡所花費的時間可能會更顯着。

  • 這個結構代表一個數據包。 您是否打算從套接字直接讀/寫到struct Packet 如果是這樣,您可能需要考慮字節順序。 發送數據包時是否必須從主機字節順序轉換為網絡字節順序,在接收數據包時反之亦然? 如果是這樣,那么您可以在可變長度結構中對數據進行字節交換。 如果您將其轉換為使用向量,則編寫用於序列化/反序列化數據包的方法是有意義的。 這些方法會將其傳輸到/從連續緩沖區傳輸,同時考慮字節順序。

  • 同樣,您可能需要考慮對齊和打包。

  • 你永遠不能子類Packet 如果這樣做,則子類的成員變量將與數組重疊。

  • 您可以使用Packet* p = ::operator new(size)::operator delete(p)代替mallocfree ,因為struct Packet是一種 POD 類型,目前無法從調用其默認構造函數和析構函數中受益. 這樣做的(潛在)好處是全局operator new使用全局 new 處理程序和/或異常處理錯誤,如果這對您很重要。

  • 可以使可變長度結構慣用語與 new 和 delete 運算符一起使用,但效果不佳。 您可以通過實現static void* operator new(size_t size, unsigned int bitlength)創建一個接受數組長度的自定義operator new ,但您仍然必須設置 bitlength 成員變量。 如果使用構造函數執行此操作,則可以使用稍微冗余的表達式Packet* p = new(len) Packet(len)來分配數據包。 與使用全局operator newoperator delete相比,我看到的唯一好處是您的代碼的客戶端可以只調用delete p而不是::operator delete(p) 只要它們被正確調用,將分配/解除分配包裝在單獨的函數中(而不是直接調用delete p )就可以了。

如果您從不向結構中添加構造函數/析構函數、賦值運算符或虛函數,則使用 malloc/free 進行分配是安全的。

它在 C++ 圈子中是不受歡迎的,但如果你在代碼中記錄它,我認為它的用法是可以的。

對您的代碼的一些評論:

struct Packet
{
    unsigned int bitlength;
    unsigned int data[];
};

如果我沒記錯的話,聲明一個沒有長度的數組是非標准的。 它適用於大多數編譯器,但可能會給您一個警告。 如果您想符合要求,請聲明您的長度為 1 的數組。

Packet* CreatePacket(unsigned int length)
{
    Packet *output = (Packet*) malloc((length+1)*sizeof(unsigned int));
    output->bitlength = length;
    return output;
}

這有效,但您沒有考慮結構的大小。 一旦向結構中添加新成員,代碼就會中斷。 最好這樣做:

Packet* CreatePacket(unsigned int length)
{
    size_t s = sizeof (Packed) - sizeof (Packed.data);
    Packet *output = (Packet*) malloc(s + length * sizeof(unsigned int));
    output->bitlength = length;
    return output;
}

並在您的數據包結構定義中寫入注釋,數據必須是最后一個成員。

順便說一句 - 使用單個分配分配結構和數據是一件好事。 這樣,您將分配數量減半,同時也改善了數據的局部性。 如果您分配大量包,這可以大大提高性能。

不幸的是,c++ 沒有提供一個很好的機制來做到這一點,所以你經常在現實世界的應用程序中遇到這樣的 malloc/free hack。

這是可以的(並且是 C 的標准做法)。

但這對 C++ 來說不是一個好主意。
這是因為編譯器會圍繞類自動為您生成一整套其他方法。 這些方法不明白你被騙了。

例如:

void copyRHSToLeft(Packet& lhs,Packet& rhs)
{
    lhs = rhs;  // The compiler generated code for assignement kicks in here.
                // Are your objects going to cope correctly??
}


Packet*   a = CreatePacket(3);
Packet*   b = CreatePacket(5);
copyRHSToLeft(*a,*b);

使用 std::vector<> 它更安全並且工作正常。
我還敢打賭,在優化器啟動后,它與您的實現一樣有效。

或者 boost 包含一個固定大小的數組:
http://www.boost.org/doc/libs/1_38_0/doc/html/array.html

如果需要,您可以使用“C”方法,但為了安全起見,請使用它以便編譯器不會嘗試復制它:

struct Packet
{
    unsigned int bytelength;
    unsigned int data[];

private:
   // Will cause compiler error if you misuse this struct
   void Packet(const Packet&);
   void operator=(const Packet&);
};

我可能會堅持使用vector<>除非最小的額外開銷(可能是您的實現上的一個額外單詞或指針)確實造成了問題。 沒有什么說你必須在構造向量后調整大小()。

但是,使用vector<>有幾個優點:

  • 它已經正確處理了復制、分配和銷毀 - 如果你自己動手,你需要確保你正確處理這些
  • 所有的迭代器支持都在那里——同樣,你不必自己滾動。
  • 每個人都已經知道如何使用它

如果您真的想防止數組在構造后增長,您可能需要考慮擁有自己的類,該類私下繼承自vector<>或具有vector<>成員,並且僅通過僅通過向 vector 方法傳遞這些位的方法公開您希望客戶能夠使用的矢量。 這應該有助於讓你快速前進,並很好地保證泄漏和不存在的東西。 如果您這樣做並發現 vector 的小開銷對您不起作用,您可以在沒有 vector 幫助的情況下重新實現該類,並且您的客戶端代碼不需要更改。

如果您真的在使用 C++,那么除了默認成員可見性之外,類和結構之間沒有實際區別 - 默認情況下,類具有私有可見性,而默認情況下結構具有公共可見性。 以下是等效的:

struct PacketStruct
{
    unsigned int bitlength;
    unsigned int data[];
};
class PacketClass
{
public:
    unsigned int bitlength;
    unsigned int data[];
};

關鍵是,您不需要 CreatePacket()。 您可以使用構造函數簡單地初始化 struct 對象。

struct Packet
{
    unsigned long bytelength;
    unsigned char data[];

    Packet(unsigned long length = 256)  // default constructor replaces CreatePacket()
      : bytelength(length),
        data(new unsigned char[length])
    {
    }

    ~Packet()  // destructor to avoid memory leak
    {
        delete [] data;
    }
};

有幾點需要注意。 在 C++ 中,使用 new 而不是 malloc。 我已經采取了一些自由並將位長度更改為字節長度。 如果這個類代表一個網絡數據包,你會更好地處理字節而不是位(在我看來)。 數據數組是一個無符號字符數組,而不是無符號整數。 同樣,這是基於我的假設,即此類代表網絡數據包。 構造函數允許您像這樣創建數據包:

Packet p;  // default packet with 256-byte data array
Packet p(1024);  // packet with 1024-byte data array

當 Packet 實例超出范圍並防止內存泄漏時,會自動調用析構函數。

這里已經提到了很多好的想法。 但是少了一個。 靈活數組是 C99 的一部分,因此不是 C++ 的一部分,盡管某些 C++ 編譯器可能提供此功能,但不能保證。 如果您找到一種以可接受的方式在 C++ 中使用它們的方法,但是您的編譯器不支持它,您也許可以退回到“經典”方式

您可能想要比矢量更輕的東西以獲得高性能。 您還希望非常具體地了解跨平台數據包的大小。 但是您也不想擔心內存泄漏。

幸運的是,boost 庫完成了大部分困難的工作:

struct packet
{
   boost::uint32_t _size;
   boost::scoped_array<unsigned char> _data;

   packet() : _size(0) {}

       explicit packet(packet boost::uint32_t s) : _size(s), _data(new unsigned char [s]) {}

   explicit packet(const void * const d, boost::uint32_t s) : _size(s), _data(new unsigned char [s])
   {
        std::memcpy(_data, static_cast<const unsigned char * const>(d), _size);
   }
};

typedef boost::shared_ptr<packet> packet_ptr;

packet_ptr build_packet(const void const * data, boost::uint32_t s)
{

    return packet_ptr(new packet(data, s));
}

免責聲明:我寫了一個小庫來探索這個概念: https : //github.com/ppetr/refcounted-var-sized-class

我們想為類型T的數據結構和類型A的元素數組分配單個內存塊。 在大多數情況下, A將只是char

為此,讓我們定義一個RAII類來分配和釋放這樣的內存塊。 這帶來了幾個困難:

  • C++分配器不提供這樣的 API。 因此,我們需要分配普通的char並將結構放在塊中。 為此, std::aligned_storage會有所幫助。
  • 內存塊必須正確對齊 因為在 C++11 中似乎沒有用於分配對齊塊的 API,我們需要通過alignof(T) - 1個字節稍微過度分配,然后使用std::align
// Owns a block of memory large enough to store a properly aligned instance of
// `T` and additional `size` number of elements of type `A`.
template <typename T, typename A = char>
class Placement {
 public:
  // Allocates memory for a properly aligned instance of `T`, plus additional
  // array of `size` elements of `A`.
  explicit Placement(size_t size)
      : size_(size),
        allocation_(std::allocator<char>().allocate(AllocatedBytes())) {
    static_assert(std::is_trivial<Placeholder>::value);
  }
  Placement(Placement const&) = delete;
  Placement(Placement&& other) {
    allocation_ = other.allocation_;
    size_ = other.size_;
    other.allocation_ = nullptr;
  }

  ~Placement() {
    if (allocation_) {
      std::allocator<char>().deallocate(allocation_, AllocatedBytes());
    }
  }

  // Returns a pointer to an uninitialized memory area available for an
  // instance of `T`.
  T* Node() const { return reinterpret_cast<T*>(&AsPlaceholder()->node); }
  // Returns a pointer to an uninitialized memory area available for
  // holding `size` (specified in the constructor) elements of `A`.
  A* Array() const { return reinterpret_cast<A*>(&AsPlaceholder()->array); }

  size_t Size() { return size_; }

 private:
  // Holds a properly aligned instance of `T` and an array of length 1 of `A`.
  struct Placeholder {
    typename std::aligned_storage<sizeof(T), alignof(T)>::type node;
    // The array type must be the last one in the struct.
    typename std::aligned_storage<sizeof(A[1]), alignof(A[1])>::type array;
  };

  Placeholder* AsPlaceholder() const {
    void* ptr = allocation_;
    size_t space = sizeof(Placeholder) + alignof(Placeholder) - 1;
    ptr = std::align(alignof(Placeholder), sizeof(Placeholder), ptr, space);
    assert(ptr != nullptr);
    return reinterpret_cast<Placeholder*>(ptr);
  }

  size_t AllocatedBytes() {
    // We might need to shift the placement of for up to `alignof(Placeholder) - 1` bytes.
    // Therefore allocate this many additional bytes.
    return sizeof(Placeholder) + alignof(Placeholder) - 1 +
           (size_ - 1) * sizeof(A);
  }

  size_t size_;
  char* allocation_;
};

一旦我們處理了內存分配的問題,我們就可以定義一個包裝類,它在分配的內存塊中初始化TA的數組。

template <typename T, typename A = char,
          typename std::enable_if<!std::is_destructible<A>{} ||
                                      std::is_trivially_destructible<A>{},
                                  bool>::type = true>
class VarSized {
 public:
  // Initializes an instance of `T` with an array of `A` in a memory block
  // provided by `placement`. Callings a constructor of `T`, providing a
  // pointer to `A*` and its length as the first two arguments, and then
  // passing `args` as additional arguments.
  template <typename... Arg>
  VarSized(Placement<T, A> placement, Arg&&... args)
      : placement_(std::move(placement)) {
    auto [aligned, array] = placement_.Addresses();
    array = new (array) char[placement_.Size()];
    new (aligned) T(array, placement_.Size(), std::forward<Arg>(args)...);
  }

  // Same as above, with initializing a `Placement` for `size` elements of `A`.
  template <typename... Arg>
  VarSized(size_t size, Arg&&... args)
      : VarSized(Placement<T, A>(size), std::forward<Arg>(args)...) {}

  ~VarSized() { std::move(*this).Delete(); }

  // Destroys this instance and returns the `Placement`, which can then be
  // reused or destroyed as well (deallocating the memory block).
  Placement<T, A> Delete() && {
    // By moving out `placement_` before invoking `~T()` we ensure that it's
    // destroyed even if `~T()` throws an exception.
    Placement<T, A> placement(std::move(placement_));
    (*this)->~T();
    return placement;
  }

  T& operator*() const { return *placement_.Node(); }
  const T* operator->() const { return &**this; }

 private:
  Placement<T, A> placement_;
};

這種類型是可移動的,但顯然不可復制。 我們可以提供一個函數來將其轉換為帶有自定義刪除器的shared_ptr 但這需要在內部為引用計數器分配另一個小內存塊(另請參閱std::tr1::shared_ptr 是如何實現的? )。

這可以通過引入一種專門的數據類型來解決,該數據類型將在單個結構中保存我們的Placement 、一個引用計數器和一個具有實際數據類型的字段。 有關更多詳細信息,請參閱我的refcount_struct.h

您應該聲明一個指針,而不是一個未指定長度的數組。

對未知大小的數組使用向量並沒有任何問題,這些數組將在初始化后修復。 恕我直言,這正是矢量的用途。 一旦你初始化了它,你就可以假裝它是一個數組,它的行為應該是一樣的(包括時間行為)。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM