从函数返回STL对象而不触发move

Return STL objects from function without triggering move

本文关键字:move 对象 函数 返回 STL      更新时间:2023-10-16

假设有一个函数返回任何本地对象,实现移动语义,例如任何STL容器,如std::vector, std::string等。例如:

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

默认情况下,tmp将被视为右值,tmp将被移动(或进行返回值优化)。

问题是如何手动覆盖和避免c++ 11的默认行为和执行复制构造函数,而不是移动到这里?一种解决方案可能是为std::vector实现一个禁用move语义的包装器类,但这似乎不是一个好的选择。

这样做的原因之一可能是调用者在另一个程序集中,如果运行时库是静态链接的,则会有两个堆。由于跨dll边界分配/删除内存,移动将导致内存断言。

问题是如何避免c++ 11的默认行为和执行复制构造函数,而不是移动到这里?

这不是默认行为。在这种情况下,默认行为是省略副本。这一举动只会在不实施NRVO的不太可能的情况下发生。

简短的回答是,你不能让return f;不移动。当您在c++中返回时,省略是默认的,如果不是,则移动它,如果不是,则复制它。如果您使用非平凡语句——甚至true?v:vstatic_cast<whatever const&>(v)——它将阻止自动移动并强制复制。但那帮不了你。

逃避行动对你没有帮助。返回对象仍然在函数内创建,并由调用代码处理。

现在,并不是所有的都失去了。您可以通过使用头文件(存在于客户端代码中)进行分配和dll安全接口(到实现)来避免这种情况。

这里我设计了一个sink类型,它批量地吸收T类型的数据。然后调用带有pvoid的函数指针,就完成了。

template<class T>
struct sink {
  void* s;
  void(*)(void*, T const*, T const*) f;
  void operator()(T const& t)const{ f(s, &t, (&t)+1); }
  void operator()(std::initializer_list<T> il) {
    f(s, il.begin(), il.end());
  }
};
template<class T, class A>>
sink<T> vector_sink( std::vector<T, A>& out ) {
  return {&out, +[](void* s, T const* b, T const* e){
    auto* pout = static_cast<std::vector<T,A>*>(s);
    pout->insert( pout->end(), b, e );
  }};
}

现在,从DLL中导出:

void make_data(sink<int> s) {
  s({1,2,3,4,5});
}
头文件中的

,暴露:

void make_data(sink<int> s);
std::vector<int> return_vector() {
  std::vector<int> r;
  make_data( vector_sink(r) );
}

,现在vector完全存在于DLL的客户端代码中。只有一个标准的布局类(由2个指针组成)跨越了DLL的障碍。

更高级的sink可以通过添加一个新函数(用于move-data-in)来区分左值和右值。但是,如果这是为了跨越DLL边界,那么这似乎是不明智的。

处理"返回"一个向量。要接受一个矢量(不附加),我建议编写array_view<int>,它包含两个int* s:类似地,标准布局,可以非常安全地跨越DLL边界。

对引用执行static_cast即可(例如使用可移动类C,可以是vector)

转换为左值引用禁用移动和NRVO

C f() {
    C c;
    return static_cast<C&>(c);
}

转换为右值引用只禁用NRVO

C f() {
    C c;
    return static_cast<C&&>(c);
}