变量大小的Struct C ++

时间:2009-03-27 03:48:23

标签: c++ struct variable-length

这是在C ++中创建可变大小结构的最佳方法吗?我不想使用vector,因为初始化后长度不会改变。

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;
}

编辑:重命名变量名称并更改代码更正确。

11 个答案:

答案 0 :(得分:10)

关于你正在做什么的一些想法:

  • 使用C风格的可变长度struct idiom允许您为每个数据包执行一次免费存储分配,这是struct Packet包含std::vector时所需数量的一半。如果要分配非常大量数据包,那么执行一半的免费存储分配/解除分配可能非常重要。如果您还在进行网络访问,那么等待网络所花费的时间可能会更加重要。

  • 此结构表示数据包。您是否计划直接从套接字读取/写入struct Packet?如果是这样,您可能需要考虑字节顺序。您在发送数据包时是否必须从主机转换为网络字节顺序,反之亦然?如果是这样,那么您可以在可变长度结构中对数据进行字节交换。如果将其转换为使用向量,则编写用于序列化/反序列化数据包的方法是有意义的。这些方法会将其传输到连续缓冲区或从连续缓冲区传输,并考虑字节顺序。

  • 同样,您可能需要考虑对齐和打包。

  • 您永远不能继承Packet。如果你这样做,那么子类的成员变量将与数组重叠。

  • 您可以使用mallocfree代替Packet* p = ::operator new(size)::operator delete(p),因为struct Packet是POD类型,目前不会受益从默认构造函数和它的析构函数调用。这样做的(潜在)好处是全局operator new使用全局新处理程序和/或异常处理错误,如果这对您很重要。

  • 可以使变长度结构惯用法与new和delete运算符一起使用,但不是很好。您可以通过实现operator new来创建一个采用数组长度的自定义static void* operator new(size_t size, unsigned int bitlength),但您仍然需要设置bitlength成员变量。如果使用构造函数执行此操作,则可以使用稍微冗余的表达式Packet* p = new(len) Packet(len)来分配数据包。与使用全局operator newoperator delete相比,我看到的唯一好处是您的代码的客户可以只调用delete p而不是::operator delete(p)。将分配/解除分配包装在单独的函数中(而不是直接调用delete p)只要它们被正确调用就可以了。

答案 1 :(得分:7)

如果您从未添加构造函数/析构函数,则使用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 hacks。

答案 2 :(得分:5)

这没关系(并且是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

答案 3 :(得分:3)

如果您愿意,可以使用“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&);
};

答案 4 :(得分:2)

如果您真正在使用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。我采取了一些自由并将比特长度改为字节长度。如果这个类代表一个网络数据包,你处理字节而不是比特(在我看来)会好得多。数据数组是unsigned char数组,而不是unsigned int。同样,这是基于我的假设,即此类表示网络数据包。构造函数允许您创建这样的数据包:

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

当Packet实例超出范围并且防止内存泄漏时,会自动调用析构函数。

答案 5 :(得分:2)

我可能只是坚持使用vector<>,除非最小的额外开销(可能是你的实现上的一个额外的单词或指针)确实是一个问题。没有什么可以说你必须在构造一个矢量后重新调整()。

然而,使用vector<>

有几个好处
  • 它已经处理了复制,分配和正确销毁 - 如果你自己推销,你需要确保正确处理这些
  • 所有迭代器支持都在那里 - 再次,你不必自己滚动。
  • 每个人都已经知道如何使用它

如果您确实希望阻止数组在构造后增长,您可能需要考虑让自己的类私有地从vector<>继承或具有vector<>成员,并且仅通过方法公开你希望客户能够使用那些向量方法的向量方法。这应该有助于让你快速行动,很好地保证泄漏和不存在的东西。如果你这样做并发现vector的小开销不适合你,你可以在没有vector的帮助下重新实现该类,你的客户端代码不需要改变。

答案 6 :(得分:1)

这里提到了许多好的想法。但是缺少一个。 Flexible Arrays是C99的一部分,因此不是C ++的一部分,尽管某些C ++编译器可能提供此功能,但不能保证这一点。如果你找到一种以可接受的方式在C ++中使用它们的方法,但是你有一个不支持它的编译器,你可能会回退到"classical" way

答案 7 :(得分:1)

你可能想要一些比矢量更轻的东西以获得高性能。您还希望非常具体地了解跨平台的数据包大小。但你也不想打扰内存泄漏。

幸运的是,升级库完成了大部分难题:

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));
}

答案 8 :(得分:0)

你应该声明一个指针,而不是一个长度未指定的数组。

答案 9 :(得分:0)

对于将在初始化后修复的未知大小的数组使用向量,没有任何错误。恕我直言,这正是矢量的用途。一旦你初始化它,你可以假装这个东西是一个数组,它应该表现相同(包括时间行为)。

答案 10 :(得分:0)

免责声明:我写了一个小型图书馆来探索这个概念:https://github.com/ppetr/refcounted-var-sized-class

我们希望为 T 类型的数据结构和 A 类型的元素数组分配单个内存块。在大多数情况下,A 只是 char

为此,让我们定义一个 RAII 类来分配和释放这样的内存块。这带来了几个困难:

  • C++ allocators 不提供此类 API。因此,我们需要分配普通的 char 并将结构放在块中。为此,std::aligned_storage 会有所帮助。
  • 内存块必须正确aligned。因为在 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_;
};

一旦我们处理了内存分配的问题,我们就可以定义一个包装类,它在分配的内存块中初始化 T 和一个 A 数组。

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。但这需要在内部为引用计数器分配另一小块内存(另请参阅 How is the std::tr1::shared_ptr implemented?)。

这可以通过引入一种专门的数据类型来解决,该数据类型将在单个结构中保存我们的 Placement、一个引用计数器和一个具有实际数据类型的字段。有关详细信息,请参阅我的 refcount_struct.h