我发现自己在下面使用这种类型的代码来防止内存泄漏,在性能,安全性,风格或......方面有什么问题吗?
我的想法是,如果我需要返回一个编辑过的字符串(就c-string而言不是std :: string),我使用一个临时的std :: string作为帮助器并将其设置为我想要的返回要保持那个暂时的活着。
下次我调用该函数时,它会将临时值重新设置为我想要的新值。由于我使用返回的c-string的方式,我只读取返回的值,从不存储它。
另外,我应该提一下,std :: string是一个实现细节,不想暴露它(所以不能返回std :: string,必须返回c-string)。
无论如何,这是代码:
NSLog(@"%@", [objects filteredArrayUsingPredicate:resultPredicate]);
答案 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::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{};
}
并且神奇地CStringA
现在是container_writer::sink<char>
的有效参数。
使用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指针。所有资源都是随时拥有的。
答案 2 :(得分:1)
如果你在多线程环境中使用你的类,这可能会适得其反。而不是那些技巧,只需按值返回std :: string。
我已经看到了关于&#39;实施细节的答案。我不同意。 std :: string不再是const char *的实现细节。这是一种提供字符串表示的方法。