保留临时std::string并返回c_str()以防止内存泄漏

Keep temporary std::string and return c_str() to prevent memory leaks

本文关键字:str 内存 泄漏 std string 保留 返回      更新时间:2023-10-16

我发现自己在下面使用这种类型的代码来防止内存泄漏,它在性能、安全性、风格或…方面有什么问题吗。。。?

这个想法是,如果我需要返回一个经过编辑的字符串(根据c字符串而不是std::字符串),我会使用一个临时std::string作为助手,并将其设置为我希望的返回值,并保持该临时字符串的有效性。

下次我调用该函数时,它会将临时值重新设置为我想要的新值。由于我使用返回的c字符串的方式,我只读取返回的值,从不存储它

另外,我应该提到,std::string是一个实现细节,不想公开它(所以不能返回std::字符串,必须返回c-string)。

无论如何,这是代码:

 //in header
class SomeClass
{
private:
    std::string _rawName;
public:
    const char* Name(); // return c-string
};
//in cpp file
std::string _tempStr; // my temporary helper std::string
const char* SomeClass::Name()
{
    return (_tempStr = "My name is: " +
            _rawName + ". Your name is: " + GetOtherName()).c_str();
}

这是一个错误。如果将指针作为返回值传递,则调用方必须保证指针在必要时保持有效。在这种情况下,如果拥有的对象被破坏,或者如果函数被第二次调用,导致生成新的字符串,指针可能会无效。

您想要避免一个实现细节,但您创建的实现细节比您想要避免的要糟糕得多。C++有字符串,请使用它们。

在C++中,不能简单地忽略对象的生存期。不能在忽略对象生存期的情况下与接口对话。

如果您认为您忽略了对象的生存期,那么几乎可以肯定您有一个bug。

您的接口忽略返回缓冲区的生存期。它会持续"足够长的时间"——"直到有人再次打电话给我"。这是一个模糊的保证,会导致非常糟糕的错误。

所有权应明确。明确所有权的一种方法是使用C风格的接口。另一种是使用C++库类型,并要求您的客户端与您的库版本相匹配。另一个是使用自定义智能对象,并保证它们在不同版本之间的稳定性。

这些都有缺点。C风格的界面很烦人。在客户端上强制使用相同的C++库是很烦人的。拥有自定义的智能对象是代码复制,并迫使您的客户端使用您编写的任何字符串类,而不是他们想要使用的任何,或编写良好的std类。

最后一种方法是类型擦除,并保证类型擦除的稳定性。

让我们看看这一选择。我们输入erase,直到分配给类似std的容器。这意味着我们忘记了我们擦除的东西的类型,但我们记得如何分配给它

namespace container_writer {
  using std::begin; using std::end;
  template<class C, class It, class...LowPriority>
  void append( C& c, It b, It e, LowPriority&&... ) {
    c.insert( end(c), b, e );
  }
  template<class C, class...LowPriority>
  void clear(C& c, LowPriority&&...) {
    c = {};
  }
  template<class T>
  struct sink {
    using append_f = void(*)(void*, T const* b, T const* e);
    using clear_f = void(*)(void*);
    void* ptr = nullptr;
    append_f append_to = nullptr;
    clear_f clear_it = nullptr;
    template<class C,
      std::enable_if_t< !std::is_same<std::decay_t<C>, sink>{}, int> =0
    >
    sink( C&& c ):
      ptr(std::addressof(c)),
      append_to([](void* ptr, T const* b, T const* e){
        auto* pc = static_cast< std::decay_t<C>* >(ptr);
        append( *pc, b, e );
      }),
      clear_it([](void* ptr){
        auto* pc = static_cast< std::decay_t<C>* >(ptr);
        clear(*pc);
      })
    {}
    sink(sink&&)=default;
    sink(sink const&)=delete;
    sink()=default;
    void set( T const* b, T const* e ) {
      clear_it(ptr);
      append_to(ptr, b, e);
    }
    explicit operator bool()const{return ptr;}
    template<class Traits>
    sink& operator=(std::basic_string<T, Traits> const& str) {
      set( str.data(), str.data()+str.size() );
      return *this;
    }
    template<class A>
    sink& operator=(std::vector<T, A> const& str) {
      set( str.data(), str.data()+str.size() );
      return *this;
    }
  };
}

现在,container_writer::sink<T>是一个非常该死的DLL安全类。它的状态是3个C样式指针。虽然它是一个模板,但它也是标准布局,标准布局基本上意味着"有一个像C结构一样的布局"。

包含3个指针的C结构是ABI安全的。

你的代码需要一个container_writer::sink<char>,在DLL中你可以为它分配一个std::stringstd::vector<char>

DLL调用代码看到container_writer::sink<char>接口,并在客户端将传递的std::string转换为它。这在客户端创建了一些函数指针,这些指针知道如何调整大小并将内容插入std::string

这些函数指针(和一个void*)通过DLL边界。在DLL方面,它们是盲目调用的。

没有分配的内存从DLL端传递到客户端,反之亦然。尽管如此,每一位数据都有与对象相关联的定义良好的生存期(RAII风格)。没有混乱的生存期问题,因为客户端控制要写入的缓冲区的生存期,而服务器则通过自动写入的回调进行写入。

如果您有一个非std样式的容器,并且希望支持container_sink,那么这很容易。将appendclear自由函数添加到您类型的命名空间中,并让它们执行所需的操作。container_sink将自动找到它们并使用它们来填充您的容器。

例如,您可以像这样使用CStringA

void append( CStringA& str, char const* b, char const* e) {
  str += CStringA( b, e-b );
}
void clear( CStringA& str ) {
  str = CStringA{};
}

并且神奇地CCD_ 17现在是取CCD_。

append的使用只是为了防止您需要更高级的容器结构。您可以编写一个container_writer::sink方法,通过让它一次馈送存储的容器固定大小的块来吃掉非连续缓冲区;它先清除,然后重复追加。

实例

现在,这不允许您返回函数的值。

要使其发挥作用,请首先执行上述操作。公开通过container_writer::sink<char>通过DLL屏障返回字符串的函数。

把它们保密。或者将它们标记为不可调用。无论什么

接下来,编写调用这些函数的inline public函数,并返回填充的std::string。这些都是纯头文件结构,因此代码位于DLL客户端中。

所以我们得到:

class SomeClass
{
private:
   void Name(container_writer::container_sink<char>);
public:
   // in header file exposed from DLL:
   // (block any kind of symbol export of this!)
   std::string Name() { 
     std::string r;
     Name(r);
     return r;
   }
};
void SomeClass::Name(container_writer::container_sink<char> s) 
{
  std::string tempStr = "My name is: " +
        _rawName + ". Your name is: " + GetOtherName();
  s = tempStr;
}

并完成。DLL接口充当C++,但实际上只是通过3个原始C指针。所有资源都是随时拥有的。

如果您在多线程环境中使用类,这可能会适得其反。不用这些技巧,只需按值返回std::string即可。

我已经看到了关于"实施细节"的答案。我不同意。std::string与const char*相比,没有更多的实现细节。这是一种提供字符串表示的方法。