保留临时 std::string 和 return c_str() 以防止内存泄漏

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

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

我的想法是,如果我需要 return 一个编辑过的字符串(就 c 字符串而言,而不是 std::string),我会使用一个临时的 std::string 作为助手,并且将其设置为我希望我的 return 成为的样子,并保持临时状态。

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

此外,我应该提一下,std::string 是一个实现细节,不想公开它(所以不能 return std::string,必须 return c-字符串).

无论如何,这是代码:

 //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();
}

如果您曾经在多线程环境中使用 class,这可能会适得其反。而不是那些技巧,只是 return std::string 按值。

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

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

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

在 C++ 中,您不能简单地忽略对象生命周期。您不能在忽略对象生命周期的同时与接口对话。

如果您认为自己忽略了对象的生命周期,那么您几乎肯定有错误。

您的界面忽略了 returned 缓冲区的生命周期。它持续 "long enough" -- "until someone calls me again"。这是一个模糊的保证,会导致严重的错误。

权属要明确。明确所有权的一种方法是使用 C 风格的接口。另一种是使用 C++ 库类型,并要求您的客户端匹配您的库版本。另一种是使用自定义智能对象,并保证其跨版本的稳定性。

这些都有缺点。 C 风格的接口很烦人。在您的客户端上强制使用相同的 C++ 库很烦人。拥有自定义智能对象是代码重复,并迫使您的客户使用 classes you 写的任何字符串,而不是他们想使用的任何字符串,或者写得很好 std一个。

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

让我们看看那个选项。我们键入擦除以分配给一个 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 class。它的状态是 3 个 C 风格的指针。虽然是模板,但也是标准布局,标准布局基本就是"has a layout like a C struct would".

包含 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

现在,这不会让您 return 来自函数的值。

要使其正常工作,请先执行上述操作。通过 container_writer::sink<char> 在 DLL 屏障上公开 return 其字符串的函数。

将它们设为私有。或者将它们标记为不可调用。随便。

接下来,编写 inline public 调用这些函数的函数,以及 return 填充的 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 接口 acts C++,但实际上只是传递 3 个原始 C 指针。任何时候都拥有所有资源。