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

时间:2012-06-08 12:11:48

标签: c++ memory-management stdstring

我正在开发一个程序,将一个重要的数据结构存储为一个带有程序定义分隔符的非结构化字符串(所以我们需要遍历字符串并提取我们需要的信息)并且我们想要转换它是一种更结构化的数据类型。

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

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

8 个答案:

答案 0 :(得分:1)

如果您正在使用std::string,则无法为结构和字符串实际分配,也无法将两者分配为一个大块。如果你使用旧的C风格的字符串,那么它是可能的。

答案 1 :(得分:1)

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

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

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

答案 2 :(得分:1)

看一下Variable Sized Struct C++ - 简短的回答是,在vanilla C ++中无法做到这一点。

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

答案 3 :(得分:1)

我不确定这是否正好解决了你的问题。一种方法是使用预先分配的缓冲区然后使用“placement new”运算符来优化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 question中指定的字符串类提供自定义分配器。 在allocate方法的实现中,使用placement new as

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;
}

约束是对于新的工作位置,在运行时分配器应该知道原始字符串地址。这可以通过在创建新字符串成员之前进行外部显式设置来实现。但是,这并不是那么优雅。

答案 4 :(得分:1)

如果您有如此严格的记忆需求,那么您将不得不放弃std::string

最好的选择是find或写basic_string_ref (a proposal for the next C++ standard library)的实现,这实际上只是一个加上大小的char *。但它具有std::basic_string的所有(非变异)函数。然后使用工厂函数分配所需的内存(结构大小+字符串数据),然后使用placement new初始化basic_string_ref

当然,您还需要一个自定义删除功能,因为您不能只将指针传递给“删除”。


鉴于previously linked to implementation of basic_string_ref(及其相关的typedef,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

答案 5 :(得分:1)

根据需要,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,但将类型信息存储在第一个条目(或前几个)中。当然,这需要在您访问数据时考虑到这一点。

答案 6 :(得分:1)

实际上,两次分配似乎太高了。有两种方法可以减少它们:

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

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

1。您可以在C ++中使用struct hack

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

从技术上讲,它需要:

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

老实说,这是艰难的。

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

足够简单:

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

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

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

答案 7 :(得分:0)

  

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

我有一种感觉,你可能没有在这里利用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;
 }