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

时间:2015-11-11 20:51:58

标签: c++ c++11 memory-leaks stdstring

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

我的想法是,如果我需要返回一个编辑过的字符串(就c-string而言不是std :: string),我使用一个临时的std :: string作为帮助器并将其设置为我想要的返回要保持那个暂时的活着。

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

另外,我应该提一下,std :: string是一个实现细节,不想暴露它(所以不能返回std :: string,必须返回c-string)。

无论如何,这是代码:

NSLog(@"%@", [objects filteredArrayUsingPredicate:resultPredicate]);

3 个答案:

答案 0 :(得分:4)

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

您希望避免实现细节,但是您创建的实现细节比您想要避免的细节更糟糕。 C ++有字符串,使用它们。

答案 1 :(得分:2)

在C ++中,你不能简单地忽略对象的生命周期。在忽略对象生存期的同时,你无法与界面交谈。

如果你认为你忽略了对象的生命周期,你几乎肯定会有一个错误。

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

所有权应该清楚。使所有权明确的一种方法是使用C风格的界面。另一种方法是使用C ++库类型,并要求您的客户端匹配您的库版本。另一种方法是使用自定义智能对象,并保证它们在版本上的稳定性。

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

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

让我们看一下这个选项。我们键入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风格的指针。虽然它是一个模板,但它也是标准布局,标准布局基本上意味着&#34;有一个类似C结构的布局会#34;。

包含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{};
}

并且神奇地CStringA现在是container_writer::sink<char>的有效参数。

使用append就是为了防止您需要更精细的容器构造。您可以编写一个container_writer::sink方法,通过让它一次为固定大小的块提供存储容器来吃非连续缓冲区;它做了一个明确的,然后重复的追加。

live example

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

要实现这一点,请先执行以上操作。公开通过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指针。所有资源都是随时拥有的。

答案 2 :(得分:1)

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

我已经看到了关于&#39;实施细节的答案。我不同意。 std :: string不再是const char *的实现细节。这是一种提供字符串表示的方法。