在调用C++/STL算法时消除不必要的副本

101*_*010 5 c++ stl visual-c++ c++11 g++4.8

  • 我编写了以下示例,以便更好地说明我的问题.

  • 在下面的代码中,我介绍了一个函数对象(即funObj).

  • funObj类的定义中,调用的一个整数成员变量id被定义为保存每个funObj构造的ID 和一个静态整数成员变量n来计算funObj创建的对象.

  • 因此,每次funObj构造对象时都会n增加一个,并将其值分配给id新创建的字段funObj.

  • 此外,我已经定义了一个默认构造函数,一个复制构造函数和一个析构函数.这三个都是打印消息stdout,以便表明他们的调用以及funObj他们所指的ID .

  • 我还定义了一个函数func,它接受类型的值对象作为输入funObj.

码:

#include <vector>
#include <iostream>
#include <algorithm>
#include <functional>

template<typename T>
class funObj {
  std::size_t id;
  static std::size_t n;
public:
  funObj() : id(++n) 
  { 
    std::cout << "    Constructed via the default constructor, object foo with ID(" << id << ")" << std::endl;
  }
  funObj(funObj const &other) : id(++n) 
  {
    std::cout << "    Constructed via the copy constructor, object foo with ID(" << id << ")" << std::endl;
  }
  ~funObj()
  { 
    std::cout << "    Destroyed object foo with ID(" << id << ")" << std::endl;
  }
  void operator()(T &elem)
  { 

  }
  T operator()()
  {
    return 1;
  }
};

template<typename T>
void func(funObj<T> obj) { obj();  }

template<typename T>
std::size_t funObj<T>::n = 0;

int main()
{
  std::vector<int> v{ 1, 2, 3, 4, 5, };
  std::cout << "> Calling `func`..." << std::endl;
  func(funObj<int>());
  std::cout << "> Calling `for_each`..." << std::endl;
  std::for_each(std::begin(v), std::end(v), funObj<int>());
  std::cout << "> Calling `generate`..." << std::endl;
  std::generate(std::begin(v), std::end(v), funObj<int>());

  // std::ref 
  std::cout << "> Using `std::ref`..." << std::endl;
  auto fobj1 = funObj<int>();
  std::cout << "> Calling `for_each` with `ref`..." << std::endl;
  std::for_each(std::begin(v), std::end(v), std::ref(fobj1));
  std::cout << "> Calling `generate` with `ref`..." << std::endl;
  std::for_each(std::begin(v), std::end(v), std::ref(fobj1));
  return 0;
}
Run Code Online (Sandbox Code Playgroud)

输出:

打电话func......

通过默认构造函数构造,ID为(1)的对象foo

ID为(1)的被破坏对象foo

打电话for_each......

通过默认构造函数构造,ID为(2)的对象foo

通过复制构造函数构造,ID为(3)的对象foo

被破坏的对象foo与ID(2)

被破坏的对象foo与ID(3)

打电话generate......

通过默认构造函数构造,ID为(4)的对象foo

通过复制构造函数构造,ID为(5)的对象foo

被破坏的对象foo与ID(5)

被破坏的对象foo与ID(4)

使用std::ref......

通过默认构造函数构造,ID为(6)的对象foo

打电话for_eachref......

打电话generateref......

被破坏的对象foo与ID(6)

讨论:

从上面的输出中可以看出,func使用类型的临时对象调用函数会funObj导致构造单个funObj对象(即使func按值传递其参数).然而,这似乎传递一个类型的临时对象时,不会是这种情况funObj,以STL算法std::for_eachstd::generate.在前一种情况下,引发复制构造函数并funObj构造一个额外的构造函数.在相当多的应用中,这种"不必要的"拷贝的产生显着地恶化了算法的性能.基于这一事实,正在提出以下问题.

问题:

  1. 我知道大多数STL算法都是按值传递参数.然而,func与之相比,它还通过值传递其输入参数,STL算法生成额外的副本.这个"不必要"的副本是什么原因?
  2. 有没有办法消除这种"不必要的"副本?
  3. 在调用时std::for_each(std::begin(v), std::end(v), funObj<int>()),func(funObj<int>())临时对象在哪个范围内funObj<int>,每个案例分别?
  4. 我试图使用std::ref以强制通过引用传递,因为你可以看到"不必要的"副本被删除.但是,当我尝试将临时对象传递给std::ref(即std::ref(funObj<int>()))时,我收到编译器错误.为什么这种陈述是非法的?
  5. 输出是使用VC++ 2013生成的.正如你所看到的那样,当调用std::for_each对象的析构函数时,它们会以相反的顺序被调用.为什么会这样?
  6. 当我运行运行GCC v4.8的Coliru上的代码时,析构函数的异常是固定的,但是std::generate不会生成额外的副本.为什么会这样?

详细信息/评论:

  • 上面的输出是从VC++ 2013生成的.

更新:

  • 我还在funObj类中添加了一个移动构造函数(参见下面的代码).

 funObj(funObj&& other) : id(other.id)
  {
    other.id = 0;
    std::cout << "    Constructed via the move constructor, object foo with ID(" << id << ")" << std::endl;
  }
Run Code Online (Sandbox Code Playgroud)
  • 我还在VC++ 2013中启用了完全优化,并在发布模式下编译.

输出(VC++ 2013):

打电话func......

通过默认构造函数构造,ID为(1)的对象foo

ID为(1)的被破坏对象foo

打电话for_each......

通过默认构造函数构造,ID为(2)的对象foo

通过移动构造函数构造,ID为(2)的对象foo

被破坏的对象foo与ID(2)

ID为(0)的被破坏对象foo

打电话generate......

通过默认构造函数构造,ID为(3)的对象foo

通过复制构造函数构造,ID为(4)的对象foo

被破坏的对象foo与ID(4)

被破坏的对象foo与ID(3)

使用std::ref......

通过默认构造函数构造,ID为(5)的对象foo

打电话for_eachref......

打电话generateref......

被破坏的对象foo与ID(5)

输出GCC 4.8

打电话func......

通过默认构造函数构造,ID为(1)的对象foo

ID为(1)的被破坏对象foo

打电话for_each......

通过默认构造函数构造,ID为(2)的对象foo

通过移动构造函数构造,ID为(2)的对象foo

被破坏的对象foo与ID(2)

ID为(0)的被破坏对象foo

打电话generate......

通过默认构造函数构造,ID为(3)的对象foo

被破坏的对象foo与ID(3)

通过默认构造函数构造,ID为(4)的对象foo

打电话for_eachref......

打电话generateref......

被破坏的对象foo与ID(4)

std::generate如果启用了优化标志并且编译处于发布模式并且除了定义了移动构造函数之外,VC++ 2013似乎生成额外的副本无关紧要.

Nik*_*iou 4

1 - 我知道大多数 STL 算法按值传递参数。然而,与同样按值传递输入参数的 func 相比,STL 算法会生成一个额外的副本。这个“不必要的”副本的原因是什么?

STL 算法返回函数对象。发生这种情况是为了可以观察到对象上的突变。您的func退货无效,因此少了一份副本。

  • 好吧,准确地说,generate不返回任何东西(参见dyp 的评论)

2 - 有没有办法消除这种“不必要的”副本?

好吧,不必要的有点太强了。函子的全部意义在于成为轻量级对象,因此副本并不重要。至于一种方法,您提供的(std::ref)将完成这项工作,可惜将生成一份副本std::ref(尽管您的对象不会被复制)

另一种方法是限定算法的调用

那么函数对象类型将是一个引用:

auto fobj1 = funObj<int>();

std::for_each<std::vector<int>::iterator, std::vector<int>::iterator, 
funObj<int>&> // this is where the magic happens !!
(std::begin(v), std::end(v), fobj1);
Run Code Online (Sandbox Code Playgroud)

3 - 当调用 std::for_each(std::begin(v), std::end(v), funObj()) 和 func(funObj()) 时,临时对象 funObj 分别位于哪个范围内?

正文std_for_each展开如下:

template<class InputIterator, class Function>
  Function for_each(InputIterator first, InputIterator last, Function fn)
{ // 1
  while (first!=last) {
    fn (*first);
    ++first;
  }
  return fn;      // or, since C++11: return move(fn);
// 2
}
Run Code Online (Sandbox Code Playgroud)

你的函数读取

template<typename T>
void func(funObj<T> obj) 
{ // 1.
    obj();  
// 2.
}
Run Code Online (Sandbox Code Playgroud)

注释12标记每种情况下的使用寿命。但请注意,如果应用返回值优化(命名或未命名),则编译器可能会生成将返回值(for_each 中的函数对象)放置在调用者的堆栈帧中的代码,因此生命周期更长。

4 - 我尝试使用 std::ref 来强制传递引用,正如您所看到的,“不必要的”副本被消除了。但是,当我尝试将临时对象传递给 std::ref (即 std::ref(funObj()))时,我收到编译器错误。为什么这样的言论是违法的?

std::ref不适用于右值引用(STL 代码如下):

template<class _Ty>
void ref(const _Ty&&) = delete;
Run Code Online (Sandbox Code Playgroud)

你需要传递一个左值

5 - 输出是使用 VC++2013 生成的。正如您所看到的,调用 std::for_each 时存在异常,对象的析构函数以相反的顺序调用。为什么会这样?

6 - 当我在运行 GCC v4.8 的 Coliru 上运行代码时,析构函数的异常已修复,但 std::generate 不会生成额外的副本。为什么会这样?

  • 检查每个编译的设置。通过优化(以及在 VS 的发布中),复制省略/消除额外副本/忽略不可观察的行为是可能的。

  • 其次(据我所知)在 VS 2013 中,仿函数for_each和生成器generate都是按值传递的(没有接受右值引用的签名),因此显然是复制省略以保存额外副本的问题。

重要的是,gcc 中的 STL 实现也没有接受右值引用的签名(如果发现有签名,请通知我)

template<typename _InputIterator, typename _Function>
_Function
for_each(_InputIterator __first, _InputIterator __last, _Function __f)
{
  // concept requirements
  __glibcxx_function_requires(_InputIteratorConcept<_InputIterator>)
  __glibcxx_requires_valid_range(__first, __last);
  for (; __first != __last; ++__first)
__f(*__first);
  return _GLIBCXX_MOVE(__f);
}
Run Code Online (Sandbox Code Playgroud)

所以我可能会在这个问题上冒险并假设,为函子定义移动语义没有效果,只有编译器优化适用于消除副本