可变大小的结构体 C++

21

这是在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;
}

编辑:重命名变量名并更正了代码。


我从这个问题中删除了C标签,因为您似乎在寻找C++答案,而技术不同。 - unwind
11个回答

9

以下是对你正在做的一些想法:

  • 使用C风格的可变长结构体习惯可以让您每个数据包执行一次自由存储分配,这比在struct Packet中包含一个std::vector所需的数量少一半。如果您要分配非常大量的数据包,则执行一半的自由存储分配/撤销可能非常重要。如果您还要进行网络访问,则等待网络的时间可能更为重要。

  • 该结构表示一个数据包。您是否计划直接从套接字读取/写入到struct Packet中?如果是,则可能需要考虑字节顺序。当发送数据包时,您是否将需要从主机字节顺序转换为网络字节顺序,以及在接收数据包时反之亦然?如果是,则可以在可变长结构体中原地交换数据。如果您将其转换为使用向量,则编写序列化/反序列化数据包的方法将是有意义的。这些方法将把它传输到/从连续缓冲区中,并考虑字节顺序。

  • 同样,您可能需要考虑对齐和填充。

  • 您永远无法子类化Packet。如果这样做,那么子类的成员变量将与数组重叠。

  • 您可以使用Packet* p = ::operator new(size)::operator delete(p)而不是mallocfree,因为struct Packet是POD类型,目前不受其默认构造函数和析构函数的影响。这样做的(潜在)好处是全局operator new使用全局new-handler和/或异常处理错误,如果这对您很重要。

  • 可变长结构体习惯可以与新的和删除的运算符一起工作,但效果不佳。您可以创建一个自定义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)是可以的,只要它们被正确调用。


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的hack方法。


嗨,这似乎是最好的解决方案,但我的编译器(mingw上的gcc)不允许我执行sizeof(Packet.data)。然而,它可以让我执行Packet test; sizeof(test.data); - Unknown
2
sizeof(Packet) 的值将是对齐要求的倍数,而不是结构体实际大小。例如 struct Foo { uint64_t magic; uint32_t length; uint8_t buf[]; }sizeof(Foo) == 16,且 sizeof(Foo) - sizeof(Foo::buf) == 16。你想要的大小应该是 offsetof(Foo, buf) == 12 - Goswin von Brederlow
@Unknown 你可以使用 sizeof(Packet::data) - yyny

4

这在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

这个用户使用boost array模板的问题在于boost :: array <>的大小是在编译时确定的。 - Michael Burr
3
如果我使用向量,那么长度成员不就变得非连续了吗? - Unknown
可能会,但不能保证。但是你为什么想要确定呢? - Benoît
不,如果没有要求的话,期望或要求特定的内存布局是愚蠢的。让编译器解决这个问题,你应该专注于如何正确使用对象。 - Martin York
3
他担心第二次赋值 new vector<int>(50) 会导致两次分配:一次是为了分配 vector 对象,另一次是为了分配由 vector 对象维护的 50 个 int 的数组。 - jmucchiello
显示剩余5条评论

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

1

我可能会建议您使用vector<>,除非额外的开销(可能是一个单独的额外字或指针)真的成为问题。一旦构造完成,没有什么规定必须调整vector<>的大小。

然而,使用vector<>有几个优点:

  • 它已经正确处理了复制、赋值和销毁 - 如果您自己编写代码,需要确保正确处理这些操作
  • 所有迭代器支持都在那里 - 再次强调,您不必自己编写代码。
  • 每个人都已经知道如何使用它

如果您真的想防止数组在构造后增长,您可以考虑拥有自己的类,该类从vector<>私有继承或具有vector<>成员,并且只通过方法公开向客户端提供对那些您希望客户端能够使用的vector部分的访问。这应该可以帮助您快速启动,并且相当有把握地避免泄漏等问题。如果您这样做并发现vector的小开销无法满足您的需求,您可以重新实现该类,而无需使用vector,您的客户端代码不应该需要更改。


1

这里已经提到了很多好的想法,但有一个遗漏。灵活数组是C99的一部分,因此不属于C ++,尽管某些C ++编译器可能提供此功能,但无法保证。如果您找到了一种可接受的方式在C++中使用它们,但是您的编译器不支持它,那么您可以回退到"传统的"方式。


1
C99被包含在C++11中。尽管我意识到你的评论早于C++11。 - dspeyer

0

免责声明:我编写了一个小型库来探索这个概念: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


0

在使用向量处理初始化后大小未知但将固定的数组时,没有任何问题。在我看来,这正是向量的用途。一旦你初始化了它,你可以假装这个东西是一个数组,它应该表现得一样(包括时间行为)。


0
如果你真正在使用C++,那么类和结构体之间没有实际区别,除了默认成员可见性 - 类默认为私有可见性,而结构体默认为公共可见性。以下是等效的:
struct PacketStruct
{
    unsigned int bitlength;
    unsigned int data[];
};
class PacketClass
{
public:
    unsigned int bitlength;
    unsigned int data[];
};

重点是,您不需要CreatePacket()。您可以使用构造函数初始化结构对象。
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。我自己改了bitlength为bytelength。如果这个类表示网络数据包,我认为最好处理字节而不是位。数据数组是无符号字符数组,而不是无符号整数数组。同样,这是基于我的假设,即这个类表示一个网络数据包。构造函数允许你像这样创建一个数据包:
Packet p;  // default packet with 256-byte data array
Packet p(1024);  // packet with 1024-byte data array

析构函数会在 Packet 实例超出作用域时自动调用,以避免内存泄漏。


最好在构造函数中使用默认值,并且只有一个,而不是使用带有默认构造函数的重复代码。 - JProgrammer
好的,你说得对。我最近一直在做C#,它不允许默认参数。我会更新这篇文章。 - Matt Davis
8
你对数据成员的初始化不能起作用,因为它不是一个指针。如果你把它改成指针,你会失去长度和数据之间连续的布局,我认为这不符合原始问题的目标。 - Mark Ransom
1
我说相反的意思——永远不要使用new[]。它和vector一样重,但你必须记得自己删除它。C++ FAQ也同意我的观点:http://www.parashift.com/c++-faq-lite/containers.html - Jimmy J
这个不起作用。变量大小的结构体必须使用malloc创建,这意味着没有构造函数。如果将它们声明为变量,则变量大小部分将设置为零。空间就是没有分配的。从堆指针初始化成员数组根本没有意义(也无法编译)。如果data是一个指针,那么这段代码将起作用,但那样你可能会使用std::vector。 - dspeyer

0

如果您需要高性能的数据结构,那么您可能需要比向量更轻的东西。您还需要非常明确地指定跨平台数据包的大小。但是您也不想为内存泄漏而烦恼。

幸运的是,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));
}

这怎么比std::vector更轻量级呢?你使用了更多的内存,并增加了一个额外的间接层。 - Jimmy J
没有例外。没有验证。您可以确定您拥有的内存是实际分配的内存(向量可能会保留更多)。数据包结构可能会发生变化。不是向量。这意味着您最终必须将向量放入消息中... - Edouard A.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接