我发现自己在下面使用这种类型的代码来防止内存泄漏,它在性能、安全性、风格或…方面有什么问题吗。。。?
这个想法是,如果我需要返回一个经过编辑的字符串(根据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::string
或std::vector<char>
。
DLL调用代码看到container_writer::sink<char>
接口,并在客户端将传递的std::string
转换为它。这在客户端创建了一些函数指针,这些指针知道如何调整大小并将内容插入std::string
。
这些函数指针(和一个void*
)通过DLL边界。在DLL方面,它们是盲目调用的。
没有分配的内存从DLL端传递到客户端,反之亦然。尽管如此,每一位数据都有与对象相关联的定义良好的生存期(RAII风格)。没有混乱的生存期问题,因为客户端控制要写入的缓冲区的生存期,而服务器则通过自动写入的回调进行写入。
如果您有一个非std
样式的容器,并且希望支持container_sink
,那么这很容易。将append
和clear
自由函数添加到您类型的命名空间中,并让它们执行所需的操作。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*相比,没有更多的实现细节。这是一种提供字符串表示的方法。