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

Implementing flexible array members with templates and base class

本文关键字:数组 组成员 实现 基类      更新时间:2023-10-16

在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作为标准布局,这不是很好。

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

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

我对这两个琐碎的解决方案不感兴趣(包括基类中指向数组开头的额外指针,以及在堆上分配数组)。

您想做的事情在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;

然后,您必须编写一些模板技巧,计算出在运行时给定N,存储intdouble以及std::stringN副本需要多大的缓冲区,所有内容都正确对齐。这并不容易。

一旦您完成了这项工作,您还需要编写构建上述所有内容的代码。如果你想变得有趣,你甚至可以公开一个完美的转发构造函数和构造函数包装器,允许在非默认状态下构建对象。

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

对于末尾的数组,您将返回一些临时数据结构,该结构重载了生成引用的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++中的要求,在C++中,存储的数据类型不必是标准布局。

通过一点类型转换,您也可以在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);

不过,我并不真的建议将其用于一般用途。(对我来说)使用这样的方案的唯一可接受的地方是以某种方式传输结构(网络或文件)。然后,我建议将它封送至具有可变长度数据的std::vector的"适当"C++对象。

您想要做的事情在C++中根本不可能实现。原因是sizeof(T)是编译时常数,所以在类型中放置数组会使其具有编译时大小。因此,正确的c++方法可以将数组保持在类型之外。请注意,只有当数组位于某个类型内部时,才有可能将其放置到堆栈中。所以所有基于堆栈的东西都受限于数组的编译时大小。(alloca可能会解决这个问题)。您最初的C版本也有类似的问题,即类型无法处理运行时大小的数组。

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

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

我知道我来晚了,但我的建议是:

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

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

我也有点晚了,但此解决方案与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的对象中使用this时要特别小心-子成员,因为你可能会无意中在后面添加另一个字段,并得到一些奇怪的错误。例如,我不建议定义一个成员本身包含Flexible的结构/类,比如这个:

class A{
  Flexible<char> a;
};
class B{
  A a;
};

因为在之后很容易犯这个错误

class B{
  A a;
  int i;
};