C++ Can标准::共享\u ptr<;标准::字符串常量>;作为引用计数的不可变字符串的有效实现?

C++ Can标准::共享\u ptr<;标准::字符串常量>;作为引用计数的不可变字符串的有效实现?,c++,string,immutability,c++-standard-library,C++,String,Immutability,C++ Standard Library,理想情况下,不可变字符串类只需要为每个字符串分配一个内存。甚至引用计数也可以存储在保存字符串本身的同一内存块中 string和shared_ptr的一个简单实现将为shared_ptr分配三个不同的内存块: 字符串缓冲区的内存 字符串对象的内存 用于引用计数的内存 现在,我知道当使用std::make_shared()时,智能实现可以将最后两个合并到一个分配中。但这仍将留下两笔拨款 如果知道字符串是不可变的,则不会重新分配字符串缓冲区,因此应该可以将其与字符串对象集成,只留下一个分配 cla

理想情况下,不可变字符串类只需要为每个字符串分配一个内存。甚至引用计数也可以存储在保存字符串本身的同一内存块中

string
shared_ptr
的一个简单实现将为
shared_ptr
分配三个不同的内存块:

  • 字符串缓冲区的内存
  • 字符串对象的内存
  • 用于引用计数的内存
现在,我知道当使用
std::make_shared()
时,智能实现可以将最后两个合并到一个分配中。但这仍将留下两笔拨款

如果知道字符串是不可变的,则不会重新分配字符串缓冲区,因此应该可以将其与字符串对象集成,只留下一个分配

class ImmutableStringAllocator;

template<typename CharT>
using immutable_string = std::basic_string<CharT, std::char_traits<CharT>, ImmutableStringAllocator>

template<size_t N>
immutable_string<char> make_immutable_string(char (&data)[N])
{
    ImmutableStringAllocator alloc(N);
    // going for basic_string::basic_string(charT *, size_t, Allocator)
    return allocate_shared<immutable_string<char>>(alloc, data, N, alloc);
}

class ImmutableStringAllocator {
    size_t len;
    size_t offset;
    char * buf;
    std::reference_wrapper<char *> ref;
public:
    // Normal Allocator stuff here
    ImmutableStringAllocator(size_t N) : len(N), buf(nullptr), offset(0), ref(buf) {}

    ImmutableStringAllocator(const ImmutableStringAllocator & other) : len(other.len), buf(nullptr), offset(other.offset), ref(other.buf) {}

    ImmutableStringAllocator operator=(const ImmutableStringAllocator & other) 
    { 
         assert(buf == nullptr); 
         temp(other); 
         swap(*this, temp); 
         return *this; 
    }

    pointer allocate(size_type n, const_void_pointer hint)
    {
         if (!ref.get()) { buf = ::new(n + len); offset = n; return buf; }
         return ref.get() + offset;
    }
}
我知道一些字符串实现已经对短字符串使用了这样的优化,但我正在寻找一个不管字符串长度都这样做的实现

我的问题是:我的推理正确吗?一个实现是否确实被允许并能够做到这一点?我可以合理地期望从一个好的质量标准库中实现这个优化吗?你知道当代的图书馆实现是如何做到这一点的吗


或者这是我必须自己实现的吗?

我认为实现这一点的唯一方法是
make_shared
,它接受运行时变量大小的数组。标准的,甚至是c++17(它为数组增加了对
shared\u ptr
的支持)

另一方面,Boost具有,它也可以接受数组大小参数。一旦你拥有了,你就是黄金;您将获得一个
共享的\u ptr
,它几乎满足了您的需求(除了实际上是一个
std::string

如果你不想使用boost,你可以自己使用。这可能没那么难

另一个要考虑的是,如果你只创建O(1)字符串,那么就永远不会更快地删除它们,并绕过原始指针(或者<代码> STD::StrugyVIEW s)。这样就避免了任何复制或篡改引用计数。(引用计数实际上很慢,因为它们使用原子操作。)


您还可以使用一种内部机制,如
std::unordered_set

您可能需要为所有分配使用一个自定义分配器

class ImmutableStringAllocator;

template<typename CharT>
using immutable_string = std::basic_string<CharT, std::char_traits<CharT>, ImmutableStringAllocator>

template<size_t N>
immutable_string<char> make_immutable_string(char (&data)[N])
{
    ImmutableStringAllocator alloc(N);
    // going for basic_string::basic_string(charT *, size_t, Allocator)
    return allocate_shared<immutable_string<char>>(alloc, data, N, alloc);
}

class ImmutableStringAllocator {
    size_t len;
    size_t offset;
    char * buf;
    std::reference_wrapper<char *> ref;
public:
    // Normal Allocator stuff here
    ImmutableStringAllocator(size_t N) : len(N), buf(nullptr), offset(0), ref(buf) {}

    ImmutableStringAllocator(const ImmutableStringAllocator & other) : len(other.len), buf(nullptr), offset(other.offset), ref(other.buf) {}

    ImmutableStringAllocator operator=(const ImmutableStringAllocator & other) 
    { 
         assert(buf == nullptr); 
         temp(other); 
         swap(*this, temp); 
         return *this; 
    }

    pointer allocate(size_type n, const_void_pointer hint)
    {
         if (!ref.get()) { buf = ::new(n + len); offset = n; return buf; }
         return ref.get() + offset;
    }
}
类不可变触发器;
模板
使用不可变的_字符串=std::basic_字符串
模板
不可变字符串使不可变字符串(字符和数据)[N])
{
可免疫链球菌alloc(N);
//选择基本字符串::基本字符串(图表*,大小,分配器)
返回allocate_shared(alloc,data,N,alloc);
}
类ImmutableStringLocator{
尺寸透镜;
尺寸偏差;
char*buf;
标准::参考_包装参考;
公众:
//这里是普通的分配器
ImmutableStringlocator(size_t N):len(N),buf(nullptr),offset(0),ref(buf){
ImmutableStringLocator(const ImmutableStringLocator&other):len(other.len)、buf(nullptr)、offset(other.offset)、ref(other.buf){}
ImmutableStringLocator运算符=(常量ImmutableStringLocator和其他)
{ 
断言(buf==nullptr);
临时工(其他);
交换(*本,临时);
归还*这个;
}
指针分配(大小\类型n,常量\无效\指针提示)
{
如果(!ref.get()){buf=::new(n+len);offset=n;返回buf;}
返回ref.get()+偏移量;
}
}

GCC 4.x已经计算了引用
std::string
:。如果使用
-D_GLIBCXX_USE_CXX11_ABI=0
进行编译,GCC的更高版本仍然有它。看看@Caleth,这不会有什么帮助,它的工作原理与
make_shared
非常类似,只是它使用了显式分配器。标准库中有一个计算了引用的不可变字符串稀罕。它的拼写是
std::runtime\u error
@T.C.,这让我几乎笑了起来…:),但字符串仍然是单独分配的。所以仍然有两个分配:一个用于对象,一个用于字符串。共享\u ptr用于分配字符串对象的分配器与字符串用于分配其存储的分配器不同吗?根据cppreference,
allocate\u shared
使用分配器的副本。如果使用C++17,您可以将您的
boost::shared_ptr
封装在一个简单的类中,这样您就可以轻松访问
std::string_视图
。感谢您指出这一点。但是,没有std::string接口是一个明显的缺点。我曾考虑过使用std::string_view,但我实际上如何获得数组的大小?@sh-Hm,看起来你实际上不能。我想如果这样做的话,您将不得不使用以null结尾的字符串。或者手动将长度存储在字符串的前几个字节中。在这一点上,我还不如自己实现整个过程。@Dan:一旦字符串不再使用,我确实希望最终删除它们。它们通常不短(通常约为1k个字符),而且设计一个所有者也不容易。然而,一旦建成,它们就不会改变。因此,首先想到的是共享所有权。