20

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

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

4

11 回答 11

9

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

  • 使用 C 风格的可变长度结构习惯用法允许您为每个数据包执行一次免费存储分配,如果struct Packet包含std::vector. 如果您要分配大量数据包,那么执行一半的空闲存储分配/解除分配可能非常重要。如果你也在做网络访问,那么等待网络所花费的时间可能会更重要。

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

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

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

  • 代替mallocand free,您可以使用Packet* p = ::operator new(size)and ::operator delete(p),因为struct Packet它是一种 POD 类型,并且当前不能从调用其默认构造函数和析构函数中受益。这样做的(潜在)好处是全局operator new使用全局 new-handler 和/或异常来处理错误,如果这对您很重要的话。

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

于 2009-03-27T05:15:01.753 回答
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。

于 2009-03-27T04:03:22.537 回答
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

于 2009-03-27T04:38:39.347 回答
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&);
};
于 2009-03-27T14:03:40.953 回答
2

我可能会坚持使用 a vector<>,除非最小的额外开销(可能是一个额外的单词或您的实现上的指针)确实造成了问题。没有什么说一旦构建了向量就必须 resize() 。

但是,有几个优点vector<>

  • 它已经正确处理复制、分配和销毁 - 如果您自己滚动,您需要确保正确处理这些
  • 所有的迭代器支持都在那里——同样,你不必自己动手。
  • 每个人都已经知道如何使用它

如果您真的想防止数组在构造后增长,您可能需要考虑拥有自己的类,该类从vector<>私有继承或具有vector<>成员,并且仅通过仅通过 thunk 到向量方法的方法公开您希望客户端的向量位能够使用。这应该可以帮助您快速进行,并且可以很好地保证泄漏和不存在的内容。如果您这样做并且发现 vector 的小开销对您不起作用,您可以在没有 vector 帮助的情况下重新实现该类,并且您的客户端代码不需要更改。

于 2009-03-27T04:45:10.873 回答
1

这里已经提到了很多好的想法。但是缺少一个。灵活数组是 C99 的一部分,因此不是 C++ 的一部分,尽管某些 C++ 编译器可能提供此功能,但不能保证这一点。如果您找到一种在 C++ 中以可接受的方式使用它们的方法,但您的编译器不支持它,您也许可以回退到“经典”方式

于 2009-03-27T09:22:20.863 回答
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));
}
于 2009-03-27T10:44:18.220 回答
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。我采取了一些自由并将位长度更改为字节长度。如果这个类代表一个网络数据包,你会更好地处理字节而不是位(在我看来)。数据数组是一个无符号字符数组,而不是无符号整数。同样,这是基于我的假设,即此类表示网络数据包。构造函数允许您像这样创建一个数据包:

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

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

于 2009-03-27T04:02:55.907 回答
0

将向量用于初始化后将修复的未知大小的数组没有任何问题。恕我直言,这正是向量的用途。一旦你初始化了它,你就可以假装它是一个数组,它的行为应该是一样的(包括时间行为)。

于 2009-03-27T20:23:30.433 回答
0

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

于 2009-03-27T04:10:17.650 回答
0

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

T我们想为 type 的数据结构和 type的元素数组分配一块内存A。在大多数情况下A将只是char.

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

  • C++分配器不提供这样的 API。因此,我们需要自己分配 plainchar并将结构放入块中。因为这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_;
};

一旦我们处理了内存分配的问题,我们就可以在分配的内存块中定义一个初始化的包装类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带有自定义删除器的 a。但这需要在内部为引用计数器分配另一小块内存(另请参阅如何实现 std::tr1::shared_ptr?)。

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

于 2021-02-07T12:26:52.037 回答