6

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

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

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

4

8 回答 8

2

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

最好的替代方法是找到或编写basic_string_ref(a proposal for the next C++ standard library)的实现,它实际上只是一个 char* 加上一个大小。但它具有 . 的所有(非变异)功能std::basic_string。然后你使用工厂函数来分配你需要的内存(你的结构体大小+字符串数据),然后使用placement new来初始化basic_string_ref.

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


鉴于先前链接到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引用字符串的 a 。

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

于 2012-06-08T13:18:23.610 回答
1

看看Variable Sized Struct C++ - 简短的回答是在 vanilla C++ 中没有办法做到这一点。

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

于 2012-06-08T12:44:23.790 回答
1

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

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

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

我不确定这是否完全解决了您的问题。您可以通过使用预分配缓冲区然后使用“放置新”运算符来优化 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;
}

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

于 2012-06-08T12:59:46.000 回答
1

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

  • 做一次分配
  • 做一个单一的动态分配

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

1.你可以struct在C++中使用hack

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

从技术上讲,它需要:

  • 禁用复制构造函数和赋值运算符
  • 构造构造函数和析构函数private,并提供工厂方法来分配和释放对象

老实说,这是一条艰难的道路。

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

很简单:

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

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

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

于 2012-06-08T13:21:44.200 回答
1

如果您使用std::string,则不能真正为结构和字符串进行一次分配,也不能将两者都分配为一个大块。如果您使用的是旧的 C 风格的字符串,那么它是可能的。

于 2012-06-08T12:18:25.990 回答
-1

本质上,这将需要一个结构体,其中包含一个描述结构体包含的数据类型的字段以及另一个包含数据本身的字符串的字段。

我有一种感觉,您可能没有在这里充分利用 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;
 }
于 2012-06-08T12:20:59.537 回答