【问题标题】:compliant variable length struct in C++兼容 C++ 中的可变长度结构
【发布时间】:2013-11-26 23:29:02
【问题描述】:

在标准 C 中,您可以使用大小为 0 的数组结束结构,然后过度分配它以向数组添加可变长度维度:

struct var
{
    int a;
    int b[];
}

struct var * x=malloc(sizeof(var+27*sizeof(int)));

如何在 C++ 中以标准(可移植)方式做到这一点? 可以限制最大可能大小,显然不必在堆栈上工作

我在想:

class var
{
...
private:
  int a;
  int b[MAX];
};

然后使用分配器或重载 new/delete 以根据所需大小分配不足:

(sizeof(var) - (MAX-27)*sizeof(int)

但是,虽然它似乎有效,但我不想维护它。

是否有一种完全标准/便携的更简洁的方式?

【问题讨论】:

  • 技术上不行,尽管“struct hack”(用大小 1 声明它,然后在你认为合适的时候过度分配)几乎适用于所有编译器。有人谈论 dynarray 类型模糊地做了这样的事情,但我相信他们已经投票反对将它包含在 C++14 中(而且它绝对不存在于 C++98、03 或 11 中)。
  • 在此处插入“无根据的实施”引用...
  • @GlennTeitelbaum:当然,理论上。然而,在实际实践中,唯一明显的区别是,将大小声明为 1 会使数学变得更简单。
  • 这个标准C是从什么时候开始的?您需要将最后一个成员声明为int b[]; 以获得灵活的数组成员;使用长度为 0 的数组是非标准扩展。
  • @claptrap:它还带来了内存局部性和单一分配(一次用于整个结构)。

标签: c++ data-structures struct standards standards-compliance


【解决方案1】:

虽然这不是直接回答您的问题——我要指出的是,在 C++ 中更好的做法是将 STL 库用于这种可变长度数组——它是安全且简单的,并且任何愿意维护的人都能理解它在你之后。

class var
{
...
private:
  int a;
  std::vector<int> b; // or use std::deque if more to your liking
};

现在您可以像其他任何课程一样对其进行新的更新;

var* myvar = new var;

您可以像使用旧类型数组一样使用它,而无需显式分配内存(尽管这不是大多数 ++ 程序员所做的)

myvar->b[0] = 123;
myvar->b[1] = 123;
myvar->b[2] = 123;

【讨论】:

  • std::vector 太过分了,因为只要有一个指针就可以做同样的事情。不幸的是,这意味着该类不再是 POD 并且有很多不必要的开销,因为一旦构建,大小就永远不会改变
  • 所有优点,但要继续“......你不能通过共享内存在进程之间共享它们,也不能对对象进行简单的二进制读取或写入,或者将其传递给 C 代码, 或将其覆盖在您希望解析的网络数据包中的数据上等......"
  • @Potatoswatter -- 没有明确的理由使用 new -- 这只是做事的一个例子,没有任何有趣的事情发生。
  • @TonyD -- 虽然这是真的,但它们是边缘情况,OP 的问题不是要求 C 集成或网络缓冲区管理,而是要求一种更清洁、更便携的方式来处理可变大小的数据.
  • @Soren:嗯,这个问题要求一种干净的便携方式来进行动态数组在结构结束时的破解,而不是替代方案,所以我们只能推测是否有'是 OP 需要坚持使用该模型的功能原因.... 很高兴有替代列出,并且 95% 的机会他们会适合。
【解决方案2】:

更简洁的方法是使用继承:

class Parent
{
  public:
    virtual int get_b(unsigned int index) = 0;
  protected:  
    int a;
};

class Child1
: public Parent
{
  public:
    int get_b(unsigned int index)
    {
        return b[index];  // Should have index bounds checking.
    }
  private:
    int b[20];
};

继承允许您调整父类成员的大小和数量。

【讨论】:

  • 根本问题是允许数组大小的运行时可变性。
【解决方案3】:

这里有针对 C:http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_051.html 的缺陷报告提交

响应是,将数组声明为您将需要的最大大小(或者实际上您也可以为整数设置最大可能大小)是一个“更安全的习惯用法”并且它是严格符合的。这个想法是,不是 over 分配并因此超出数组的声明大小,您实际上 under 分配并且仅访问数组声明边界内的内存。

这应该适用于 C++,只要它没有改变那些规则,它不应该有这些规则,因为它应该与 C 非常兼容。如果有人知道 C++ 特定的东西会使这个解决方案无效,请通知我。

只要您将此实现隐藏在定义良好的接口后面,就不会有任何维护问题。

【讨论】:

  • 它在 C++ 中不起作用,因为它将构造与生命周期的概念联系在一起。类的构造函数有权将整个数组的内存归零,即使标准没有要求也是如此。不存在没有存储其所有子对象的对象。
  • @Potatoswatter - 似乎很清楚这确实符合并且可以工作并反映我试图改进的代码。没有理由相信,如果您使用这种技术,您会编写一个超越虚拟数组边界的构造函数
  • @GlennTeitelbaum 什么清楚地表明这符合?答案顶部的链接适用于 C,而不是 C++。
  • @Potatoswatter 你是对的:“分配函数尝试分配请求的存储量。如果成功,它将返回一个存储块的开始地址,其长度以字节为单位至少与请求的大小一样大。”
【解决方案4】:

是的,您可以,尽管您不能将其声明为数组成员。您可以使用参考:

struct s {
    int ( & extra_arr )[];

    s() : extra_arr( reinterpret_cast< int (&)[] >( this[1] ) {}
};

实际上,这将使用指针的存储价值,尽管理论上它不需要。这门课不是POD,归结于理论和实践之间的差异。


您也可以将reinterpret_cast 放入非静态成员函数中:

struct s {
    int ( & get_extra() )[]
        { return reinterpret_cast< int (&)[] >( this[1] ); }

    int const ( & get_extra() const )[]
        { return reinterpret_cast< int const (&)[] >( this[1] ); }
};

现在访问需要函数调用语法(内联将消除除调试构建之外的机器代码差异),但不会浪费存储空间,对象将是 POD,除非 POD 规则有一些其他例外。

通过一些 ABI 调整,例如 #pragma pack,这可以获得完全的 C 二进制兼容性。无论如何,序列化应用程序通常都需要进行此类调整。

这也支持 const 正确性,而之前的解决方案允许修改 const 对象(因为它不知道数组是同一对象的一部分)。

样板文件可以泛化为 CRTP 基类(在 C++11 中甚至仍允许派生类为 POD),或扩展为定义 C++ 访问器或 C 灵活成员的预处理器宏。


请注意,这些解决方案都没有比原始 C 做更多的事情。特殊的成员函数不会复制灵活数组,并且类不支持函数参数或子对象。

【讨论】:

  • “并且对象可能是 POD”...“逻辑”对象现在具有编译器知道的内存区域之外的状态,因此 POD 暗示的大部分工作仍然会被破坏。
  • @TonyD 这些东西(即memcpy(…, sizeof (T)) 复制)在使用 C 时无论如何都不适用于灵活数组。POD 本质上保证 C 操作和构造继续以与 C 中相同的方式工作,并且在使用我的第二个解决方案时仍然如此。在进行逐字节内存操作时,您只需要进行一些额外的地址和大小计算。
  • @GlennTeitelbaum 查看我对 uk4321 答案的评论。 b[MAX] 在 C++ 中非常糟糕,因为每次都需要分配整个最坏情况的数组。 (不这样做对于实际练习来说风险太大。)
  • 我的意思是说“对象可能是 POD”不仅意味着 memcpy 等可以在对象上手动使用,而且编译器生成的复制构造函数、赋值运算符、析构函数等。就足够了。说“在使用 C 时无论如何都不要使用灵活的数组”并不会使您的实现更像 POD。我只是认为这种说法会让读者感到困惑;-.
  • @TonyD C++ 中由复制构造函数和赋值运算符提供的语义不受 C 中灵活数组的本机运算符支持。灵活数组也不能按值传递。这是他们使用的一部分,用户在使用之前必须知道。构造函数和析构函数被定义为对 POD 对象不做任何事情(或者在任何情况下,实现都不能要求它们运行)。
【解决方案5】:

简单地做 C 方式的变体有什么问题?

如果结构必须保持纯粹的 POD,C 方式就可以了。

struct var
{
    int a;
    int b[1];

    static std::shared_ptr<var> make_var(int num_b) {
        const extra_bytes = (num_b ? num_b-1 : 0)*sizeof(int);
        return std::shared_ptr<var>(
                new char[sizeof(var)+extra_bytes ],
                [](var* p){delete[]((char*)(p));});
}

因为它是一个 POD,所以一切都像在 C 中一样工作。


如果b 不能保证是 POD,那么事情会变得更有趣。我还没有测试过它,但它看起来或多或少是这样的。请注意,make_var 依赖于 make_unique,因为它使用 lambda 析构函数。没有这个你可以让它工作,但它是更多的代码。这就像 C 的方式一样,除了它使用构造函数和析构函数干净地处理可变数量的类型,并处理异常

template<class T>
struct var {
    int a;

    T& get_b(int index) {return *ptr(index);}
    const T& get_b(int index) const {return *ptr(index);}

    static std::shared_ptr<var> make_var(int num_b);
private:
    T* ptr(int index) {return static_cast<T*>(static_cast<void*>(&b))+i;}
    var(int l);
    ~var();
    var(const var&) = delete;
    var& operator=(const var&) = delete;

    typedef typename std::aligned_storage<sizeof(T), std::alignof(T)>::type buffer_type;
    int len;
    buffer_type b[1];
};
template<class T> var::var(int l)
    :len(0)
{
    try {
        for (len=0; len<l; ++len)
            new(ptr(i))T();
    }catch(...) {
        for (--len ; len>=0; --len)
            ptr(i)->~T();
        throw;
    }
}
template<class T> var::~var()
{
    for ( ; len>=0; --len)
        ptr(i)->~T();
}
template<class T> std::shared_ptr<var> var::make_var(int num_b)
{
    const extra_bytes = (num_b ? num_b-1 : 0)*sizeof(int);
    auto buffer = std::make_unique(new char[sizeof(var)+extra_bytes ]);
    auto ptr = std::make_unique(new(&*buffer)var(num_b), [](var*p){p->~var();});
    std::shared_ptr<var> r(ptr.get(), [](var* p){p->~var(); delete[]((char*)(p));});
    ptr.release();
    buffer.release;
    return std::move(r);
}

由于这是未经测试的,它甚至可能无法编译,并且可能存在错误。我通常会使用std::unique_ptr,但我懒得制作适当的独立删除器,而当删除器是 lambda 时,unique_ptr 很难从函数返回。如果您想使用这样的代码,请使用适当的独立删除器。

【讨论】:

  • 这样就不会泄露内存。使用智能指针而不是原始指针来拥有资源
【解决方案6】:

好的 - (因为我不确定,所以没有提出问题)因为在我看来,从目前的答案来看,目前没有比 over 分配更好的方法,我想知道如果这有助于维护:

template <class BASE, class T>
class dynarray
{
public:
    BASE base;
    const size_t size;
    T data[1]; // will be over allocated

    static dynarray * create(size_t data_size)
    {
        return new(data_size) dynarray(data_size);
    }
    void operator delete(void *p)
    {
        ::operator delete(p);
    }
private:
    void * operator new (size_t full_size, size_t actual)
    {
        if (full_size != sizeof(dynarray))
        {
            // inheritence changed size - allocate it all
            return ::operator new(sizeof(dynarray));
        }

        return ::operator new(sizeof(dynarray) + (actual-1)*sizeof(T));
    }
    void operator delete(void *p, size_t) // matching delete
    {
        ::operator delete(p);
    }
    dynarray(size_t data_size) : size(data_size)
    {
    }
};

用法有点生硬,但可能更好:

typedef dynarray<double,int,27> dyn;
dyn * x=dyn::create(7);
x->data[5]=28;
x->base=5.3;

编辑:将实现从 under 分配更改为 over 分配

【讨论】:

  • “分配中”,即对 T 类型的对象使用少于 sizeof(T) 字节,不起作用。此类很可能会崩溃,而您要求符合标准。请提出一个新问题,因为回答者通常不会扫描提问者的答案。
  • @Potatoswatter 这怎么可能比int * x = new int[5] 更容易崩溃。我找不到要求您为任何类 T 至少分配 sizeof(T) 的地方,无论是在标准中还是在实现中。只要求您不要访问超出分配数量的任何内存。我对此进行了测试,它确实有效。
  • 允许实现将自己的数据存储在类类型末尾的填充中。你真的检查过标准吗? §3.8/1(“对象生命周期”,第 1 段)说“类型 T 的对象的生命周期开始于: — 获得类型 T 的正确对齐和大小的存储......”这是你应该看到的第一件事查找时。
  • @Potatoswatter 我以不同的方式解释了正确的大小(它没有直接定义),我应该看的地方正在分配中 - 它确实说返回最小请求大小(第一个参数) - 啊。因此,似乎没有标准的方法来分配不足。并且过度分配“struct hack”在理论上具有无类型的数据访问权限,但这是严格不允许的吗?
  • 更改为过度分配 - 用于更多 cmets
【解决方案7】:

另一种方法是使用placement new

#include <cstdlib>

class var
{
    ...
private:
    int a;
    int b[1];
};

var * x = new(malloc(sizeof(var) + (27-1)*sizeof(var::b))) var;

在这种情况下,在分配的内存上调用构造函数

删除结构使用:

x->~var(); // only if var have a destructor
free(x);

或者更好,给 var 添加一个删除操作符并使用 delete:

struct var {
    ...
    operator delete(void* ptr) throw() { free(ptr); }
};
var * x = ...
delete x;

最好和正确的方法是使用静态函数来创建实例并将构造函数私有化: 类变量 { 民众: ... 静态 var * create(int size, ) throw() { 新的(malloc(sizeof(var) + (27-1)*sizeof(b)))) var(); } 无效运算符删除(无效*ptr){免费(ptr); }

private:
    int a;
    int b[1];
    var(<args>) { ... }
};

var * x = var::create(27);
delete x;

注意:我使用大小为 1 的数组,因为并非所有编译器都支持未定义和 0 大小的数组。

【讨论】:

    【解决方案8】:
    template <size_t MAX>
    class var
    {
       ...
    private:
      int a;
      int b[MAX];
    };
    

    在每个模板实例化中,MAX 是一个可以在循环中使用的常量。然后你可以构造任意长度的变量。

    var<7> v7;
    var<100> v100;
    

    或者typedef他们

    typedef var<10> myVar;
    

    【讨论】:

      猜你喜欢
      • 2014-11-14
      • 2015-08-09
      • 1970-01-01
      • 1970-01-01
      • 2011-10-03
      • 1970-01-01
      • 1970-01-01
      • 2023-03-04
      相关资源
      最近更新 更多