在单个分配中分配包含字符串的结构

Allocate a struct containing a string in a single allocation

本文关键字:分配 结构 字符串 单个 包含      更新时间:2023-10-16

我正在开发一个程序,该程序将重要的数据结构存储为带有程序定义的分隔符的非结构化字符串(因此我们需要遍历字符串并提取我们需要的信息),我们希望将其转换为更结构化的数据类型。

从本质上讲,这将需要一个结构,其中包含一个描述结构包含哪种数据的字段,另一个字段是包含数据本身的字符串。字符串的长度在分配时始终是已知的。我们通过测试确定,将每种数据类型所需的分配数量加倍是不可接受的成本。有没有办法在单个分配中为结构和结构中包含的 std::string 分配内存?如果我们使用 cstring,我只会在结构中有一个 char *,并在为结构体和字符串分配一个足够大的块后将其指向结构的末尾,但如果可能的话,我们更喜欢 std::string。

我的大部分经验都是关于C的,所以请原谅这里显示的任何C++无知。

如果你有如此严格的内存需求,那么你将不得不放弃std::string

最好的选择是查找或编写 basic_string_ref 的实现(下一个 C++ 标准库的建议),它实际上只是一个 char* 和一个大小。但它具有std::basic_string的所有(非变异)功能。然后使用工厂函数分配所需的内存(结构大小 + 字符串数据),然后使用放置 new 初始化basic_string_ref

当然,您

还需要自定义删除功能,因为您不能只传递指向"删除"的指针。


鉴于之前链接到basic_string_ref(及其关联的 typedefs,string_ref)的实现,下面是一个工厂构造函数/析构函数,用于某些需要具有字符串的 T 类型:

template<typename T> T *Create(..., const char *theString, size_t lenstr)
{
  char *memory = new char[sizeof(T) + lenstr + 1];
  memcpy(memory + sizeof(T), theString, lenstr);
  try
  {
    return new(memory) T(..., string_ref(theString, lenstr);
  }
  catch(...)
  {
    delete[] memory;
    throw;
  }
}
template<typename T> T *Create(..., const std::string & theString)
{
  return Create(..., theString.c_str(), theString.length());
}
template<typename T> T *Create(..., const string_ref &theString)
{
  return Create(..., theString.data(), theString.length());
}
template<typename T> void Destroy(T *pValue)
{
  pValue->~T();
  char *memory = reinterpret_cast<char*>(pValue);
  delete[] memory;
}

显然,您需要自己填写其他构造函数参数。并且类型的构造函数需要采用引用字符串的string_ref

如果你使用 std::string ,你不能真正为结构和字符串做一个分配,你也不能让两者的分配成为一个大块。如果您使用的是旧的 C 样式字符串,这是可能的。

如果我理解正确的话,您是在说通过分析,您已经确定必须在数据结构中分配一个string和另一个数据成员这一事实会给您的应用程序带来不可接受的成本。

如果确实如此,我可以想到几个解决方案。

  1. 您可以在程序开始之前预先分配所有这些结构。 将它们保存在某种固定集合中,这样它们就不会被复制构造,并在stringreserve足够的缓冲区来保存数据。
  2. 尽管看起来很有争议,但您可以使用旧的 C 样式char数组。 似乎您首先使用string的原因很多,即内存管理。 但是,在您的情况下,由于您在启动时知道所需的缓冲区大小,因此您可以自己处理。 如果您喜欢string提供的其他设施,请记住,其中大部分设施仍然在<algorithm>可用。

看看可变大小的结构C++ - 简短的回答是,在香草C++中没有办法做到这一点。

你真的需要在堆上分配容器结构吗? 将它们放在堆栈上可能会更有效,因此根本不需要分配它们。

事实上,

两个分配似乎太高了。不过,有两种方法可以减少它们:

  • 执行单个分配
  • 执行单个动态分配

它可能看起来没有那么不同,所以让我解释一下。

1.您可以使用struct黑客C++

  • 是的,这不是典型的C++
  • 是的,这需要特别小心

从技术上讲,它需要:

  • 禁用复制构造函数和赋值运算符
  • 使构造函数和析构函数private并提供用于分配和解除分配对象的工厂方法

老实说,这是艰难的方式。

2. 可以避免动态分配外部struct

足够简单:

struct M {
    Kind _kind;
    std::string _data;
};

然后在堆栈上传递M实例。移动操作应保证不复制std::string(始终可以禁用复制以确保复制)。

此解决方案要简单得多。唯一(轻微)缺点是内存局部性...但另一方面,堆栈的顶部已经在 CPU 缓存中。

C 样式字符串始终可以根据需要转换为 std::string。事实上,分析结果很有可能是由于数据的碎片化,而不仅仅是分配的数量,并且按需创建std::string将是有效的。当然,不知道您的实际应用程序这只是一个猜测,实际上在测试之前无法知道这一点。我想象一个班级

class my_class {
    std::string data() const { return self._data; }
    const char* data_as_c_str() const // In case you really need it!
    { return self._data; }
private:
    int _type;
    char _data[1];
};

请注意,我在数据布局中使用了一个标准的聪明 C 技巧:只要您的工厂函数为其分配额外的空间,_data只要您想要它。IIRC,C99甚至为它提供了一个特殊的语法:

struct my_struct {
    int type;
    char data[];
};

这很有可能与您的C++编译器一起使用。(这是C++11标准吗?

当然,如果你这样做,你真的需要让所有的构造函数私有并成为你的工厂函数的朋友,以确保工厂函数是实际实例化my_class的唯一方法——如果没有数组的额外内存,它就会被破坏。您肯定也需要将operator=设为私有,或者谨慎地实施它。


重新考虑数据类型可能是个好主意。

例如,您可以做的一件事是,不要尝试将char数组放入结构化数据类型,而是改用智能引用。一个看起来像

class structured_data_reference {
public:
    structured_data_reference(const char *data):_data(data) {}
    std::string get_first_field() const {
        // Do something interesting with _data to get the first field
    }
private:
    const char *_data;
};

您还需要使用其他构造函数和赋值运算符做正确的事情(可能禁用赋值,并实现一些合理的移动和复制)。并且您可能需要引用计数指针(例如 std::shared_ptr ) 贯穿整个代码,而不是裸指针。


另一个可能的技巧是只使用 std::string ,但将类型信息存储在第一个条目(或前几个条目)中。当然,这需要在访问数据时考虑到这一点。

我不确定这是否完全解决了您的问题。一种优化C++内存分配的方法,方法是使用预分配的缓冲区,然后使用"放置新"运算符。我试图按照我的理解解决你的问题。

 unsigned char *myPool = new unsigned char[10000];
 struct myStruct
 {
    myStruct(char* aSource1, char* aSource2)
    {
        original = new (myPool) string(aSource1); //placement new
        data = new (myPool) string(aSource2); //placement new
    }
    ~myStruct()
    {
        original = NULL; //no deallocation needed
        data = NULL; //no deallocation needed
    }
    string* original;
    string* data;
};
int main()
{
    myStruct* aStruct = new (myPool) myStruct("h1", "h2");
    //  Use the struct
    aStruct = NULL; //  No need to deallocate
    delete [] myPool;
    return 0;
}

[编辑] 之后,NicolBolas的评论,问题更加清晰。我决定再写一个答案,尽管实际上它并不比使用原始字符数组有利。但是,我仍然认为这完全在规定的限制范围内。想法是为本 SO 问题中指定的字符串类提供自定义异体。在分配方法的实现中,使用放置 new 作为

pointer allocate(size_type n, void * = 0) 
{
    // fail if we try to allocate too much
    if((n * sizeof(T))> max_size()) { throw std::bad_alloc(); }
    //T* t = static_cast<T *>(::operator new(n * sizeof(T)));
    T* t = new (/* provide the address of the original character buffer*/) T[n];
    return t;
}

约束是,要使新放置正常工作,原始字符串地址应在运行时为异位者所知。这可以通过在创建新字符串成员之前进行外部显式设置来实现。但是,这并不那么优雅。

从本质上讲,这将需要一个结构,其中包含一个描述结构包含哪种数据的字段,另一个字段是包含数据本身的字符串。

我有一种感觉,也许你没有在这里最大限度地利用C++的类型系统。它看起来和感觉都非常C-ish(我知道这不是一个合适的词)。我没有具体的例子可以在这里发布,因为我对你试图解决的问题一无所知。

有没有办法在单个分配中为结构和结构中包含的 std::string 分配内存?

我相信您正在担心结构分配,然后将字符串复制到结构成员?理想情况下,这不应该发生(但当然,这取决于您初始化成员的方式和时间)。C++11 支持移动构造。这应该会处理您担心的任何额外字符串副本。

你真的应该发布一些代码,使这个讨论值得:)

具有程序定义分隔符的非结构化字符串的重要数据结构

一个问题:这个字符串是可变的吗?如果没有,您可以使用稍微不同的数据结构。不要存储此重要数据结构的某些部分的副本,而是存储指向分隔符的此字符串的索引/迭代器。

 // assume that !, [, ], $, % etc. are your program defined delims
 const std::string vital = "!id[thisisdata]$[moredata]%[controlblock]%";
 // define a special struct
 enum Type { ... }; 
 struct Info {
     size_t start, end;
     Type type;
     // define appropriate ctors
 };
 // parse the string and return Info obejcts
 std::vector<Info> parse(const std::string& str) {
      std::vector<Info> v;
      // loop through the string looking for delims
      for (size_t b = 0, e = str.size(); b < e; ++b) {
            // on hitting one such delim create an Info
            switch( str[ b ] ) {
                case '%':
                  ... 
                case '$;:    
                // initializing the start and then move until
                // you get the appropriate end delim
            }
            // use push_back/emplace_back to insert this newly
            // created Info object back in the vector
            v.push_back( Info( start, end, kind ) );
      }
      return v;
 }