使用模板和基类实现灵活的数组成员

时间:2013-07-02 11:44:33

标签: c++ flexible-array-member

在C99中,您通常会看到以下模式:

struct Foo {
    int var1;
    int var2[];
};

Foo * f = malloc(sizeof(struct Foo) + sizeof(int)*n);
for (int i=0; i<n; ++i) {
    f->var2[i] = p;
}

但不仅是这个糟糕的C ++,它也是非法的。

你可以在C ++中实现类似的效果:

struct FooBase {
    void dostuff();

    int var1;
    int var2[1];
};

template<size_t N>
struct Foo : public FooBase {
    int var2[N-1];
};

虽然这样可行(在FooBase的方法中你可以访问var2[2]var2[3]等),它依赖Foo作为标准布局,这不是很漂亮。< / p>

这样做的好处是,非模板化函数可以通过Foo*并调用在FooBase*上运行的方法来接收任何var2而不进行转换,并且内存都是连续的(这可能很有用)。

有没有更好的方法来实现这一点(这是合法的C ++ / C ++ 11 / C ++ 14)?

我对两个简单的解决方案不感兴趣(包括基类中的一个额外指针到数组的开头,并在堆上分配数组)。

5 个答案:

答案 0 :(得分:5)

在C ++中,您想要做的事情可能并非易事,而struct的界面不是struct样式界面。

就像std::vector如何占用一块内存并将其重新格式化为非常类似于数组的内容,然后重载运算符以使其自身看起来像数组一样,您也可以这样做。

访问您的数据将通过访问者。您将在缓冲区中手动构建您的成员。

您可以从一系列“标签”和数据类型开始。

struct tag1_t {} tag1;
struct tag2_t {} tag2;
typedef std::tuple< std::pair< tag1_t, int >, std::pair<tag2_t, double> > header_t;

然后,我们将解释为“在标题部分之后,我们有一个数组”的更多类型。我想大量改进这种语法,但现在最重要的部分是建立编译时列表:

struct arr_t {} arr;
std::tuple< header_t, std::pair< arr_t, std::string > > full_t;

然后你必须写一些模板mojo,在运行时给出N,你需要多大的缓冲区才能存储intdouble然后是N std::string个副本,所有内容都正确对齐。这并不容易。

完成后,您还需要编写构造上述所有内容的代码。如果你想得到想象,你甚至会暴露出一个完美的转发构造函数和构造函数包装器,允许在非默认状态下构造对象。

最后,编写一个接口,根据我注入上述tuple s的标签找到构造对象的内存偏移量,reinterpret_cast s将原始内存转换为对数据类型的引用,并返回该引用(在const和非const版本中)。

对于最后的数组,您将返回一些重载operator[]的临时数据结构,该结构会生成引用。

如果你看看std::vector如何将内存块转换为数组,并将其与boost::mpl如何安排标记到数据地图混合,然后手动混乱以保持正确一致,每一步都具有挑战性但并非不可能。我在这里使用的凌乱语法也可以在某种程度上得到改进。

结束界面可能是

Foo* my_data = Foo::Create(7);
my_data->get<tag1_t>(); // returns an int
my_data->get<tag2_t>(); // returns a double
my_data->get<arr_t>()[3]; // access to 3rd one

可以通过一些重载来改进:

Foo* my_data = Foo::Create(7);
int x = my_data^tag1; // returns an int
double y = my_data^tag2; // returns a double
std::string z = my_data^arr[3]; // access to 3rd std::string

但是所涉及的努力相当大,才能实现这一目标,而且所需的许多事情都会非常糟糕。

基本上,为了解决你所描述的问题,我必须在C ++中手动重建整个C ++ / C结构布局系统,一旦你完成了这个,就不难注入“任意长度数组”结束”。甚至可以在中间注入任意长度的数组(但这意味着找到结构成员通过该数组的地址是一个运行时问题:但是,因为我们的operator^被允许运行任意代码,并且你的结构可以存储数组的长度,我们可以做到这一点)。

然而,我不能想到一种更简单,可移植的方式来做你在C ++中所要求的事情,其中​​存储的数据类型不必是标准布局。

答案 1 :(得分:1)

通过一些类型转换,您也可以在C ++中使用C模式。

只需将数组的初始大小设为1,然后使用new char[...]分配结构指针:

struct Foo {
    int var1;
    int var2[1];
};

Foo* foo_ptr = reinterpret_cast<Foo*>(new char[sizeof(Foo) + sizeof(int) * (n - 1)]);

然后你当然应该在释放结构时施放它:

delete[] reinterpret_cast<char*>(foo_ptr);

我不建议将它作为一般用途。唯一可以接受(对我来说)使用这种方案的地方是以某种方式传输结构(网络或文件)。然后我建议将其编组到/来自“适当的”C ++对象,其中std::vector用于可变长度数据。

答案 2 :(得分:0)

在C ++中根本不可能做什么。原因是sizeof(T)是编译时常量,因此将数组放在类型中会使其具有编译时大小。所以正确的c ++方式可以使数组保持在类型之外。请注意,只有在某种类型的内部才能将数组放入堆栈。因此,基于堆栈的所有内容都限于数组的编译时大小。 (alloca可能会解决这个问题)。您的原始C版本也有类似的问题,类型无法处理运行时大小的数组。

这也是C ++中可变长度数组的处理。不支持,因为它打破了sizeof,c ++类依赖sizeof进行数据成员访问。任何不能与c ++类一起使用的解决方案都不好。 std :: vector没有这样的问题。

请注意,c ++ 11中的constexpr使自定义数据类型中的偏移量计算变得相当简单 - 编译时限制仍然存在。

答案 3 :(得分:0)

我知道我有点迟到了,但我的消息是:

template<size_t N>
struct Foo {
    int var1;
    std::array<int,N> var2;
};

std::array将数据存储为int v[N];(不在堆中),因此将其转换为字节流不会有问题

答案 4 :(得分:0)

我也迟到了,但这个解决方案与C的灵活数组兼容(如果你当然使用预处理器):

#include <cstdlib>
#include <iostream>

using namespace std;

template <typename T>
class Flexible 
{
public:
   Flexible(){}
   ~Flexible(){}
   inline T & operator[](size_t ind){
      return reinterpret_cast<T*>(this)[ind];
   }
   inline Flexible<T> * getThis() { return this; }
   inline operator T * () { return reinterpret_cast<T*>(this); }
};

struct test{
   int a;
   Flexible<char> b;
};

int main(int argc, char * argv[]){
   cout << sizeof(test) << endl;
   test t;
   cout << &t << endl;
   cout << &t.a << endl;
   cout << &t.b << endl;
   cout << t.b.getThis() << endl;
   cout << (void*)t.b << endl;
   test * t2 = static_cast<test*>(malloc(sizeof(test) + 5));
   t2->b[0] = 'a';
   t2->b[1] = 'b';
   t2->b[2] = 0;
   cout << t2->b << endl;
   return 0;
}

(在GCC上测试过,并与clang++ -fsanitize=undefined进行了对比,我认为除了reinterpret_cast部分之外没有理由它不会是标准的......)

注意:如果它不是结构的最后一个字段,则不会出现错误。特别注意在包含此结构的对象中使用它作为sub-sub -...-子成员,因为您可能会无意中添加另一个字段并获得一些奇怪的错误。例如,我不建议使用一个本身包含Flexible的成员来定义结构/类,例如:

class A{
  Flexible<char> a;
};

class B{
  A a;
};

因为在以下情况之后很容易犯这个错误:

class B{
  A a;
  int i;
};