5
  • 为了更好地说明我的问题,我编写了以下示例。

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

  • funObj类的定义id中,定义了一个完整的成员变量来保存每个成员的 IDfunObj构造的 ID,并定义一个静态整数成员变量n来计算funObj创建的对象。

  • 因此,每次funObj构造对象时n都会增加 1,并将其值分配给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;
}

输出:

打电话func...

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

销毁 ID(1) 的对象 foo

打电话for_each...

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

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

销毁 ID(2) 的对象 foo

销毁 ID(3) 的对象 foo

打电话generate...

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

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

销毁 ID(5) 的对象 foo

已销毁 ID(4) 的对象 foo

使用std::ref...

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

for_eachref...通话

generateref...通话

销毁 ID(6) 的对象 foo

讨论:

正如您从上面的输出中看到的那样,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 的Colirustd::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;
  }

  • 我还在 VC++2013 中开启了全面优化,并在发布模式下编译。

输出(VC++2013):

打电话func...

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

销毁 ID(1) 的对象 foo

打电话for_each...

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

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

销毁 ID(2) 的对象 foo

已销毁 ID(0) 的对象 foo

打电话generate...

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

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

已销毁 ID(4) 的对象 foo

销毁 ID(3) 的对象 foo

使用std::ref...

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

for_eachref...通话

generateref...通话

销毁 ID(5) 的对象 foo

输出 GCC 4.8

打电话func...

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

销毁 ID(1) 的对象 foo

打电话for_each...

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

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

销毁 ID(2) 的对象 foo

已销毁 ID(0) 的对象 foo

打电话generate...

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

销毁 ID(3) 的对象 foo

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

for_eachref...通话

generateref...通话

已销毁 ID(4) 的对象 foo

std::generate如果优化标志打开并且编译处于发布模式,并且除了定义了移动构造函数的事实,VC++2013 似乎会生成额外的副本。

4

2 回答 2

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);

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
}

你的函数读取

template<typename T>
void func(funObj<T> obj) 
{ // 1.
    obj();  
// 2.
}

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

4 - 我尝试使用 std::ref 来强制传递引用,正如您所见,“不必要的”副本已被消除。但是,当我尝试将临时对象传递给 std::ref(即 std::ref(funObj()))时,会出现编译器错误。为什么这样的陈述是非法的?

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

template<class _Ty>
void ref(const _Ty&&) = delete;

你需要传递一个左值

5 - 使用 VC++2013 生成输出。如您所见,调用 std::for_each 时出现异常,对象的析构函数以相反的顺序调用。为什么呢?

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

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

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

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

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

所以我可能会在这个问题上犹豫不决并假设,为你的仿函数定义移动语义没有任何效果,只有编译器优化适用于消除副本

于 2014-05-12T16:48:45.607 回答
3

C++11 中引入的移动语义在很大程度上缓解了这组“不必要的”副本。如果你为你的函数对象定义了一个函数对象(即使/特别是如果它是一个临时对象)move constructor,STL将阻止复制的发生。move这将允许您在不牺牲太多性能的情况下使用具有值语义的 STL 算法。它还允许您根据需要使用临时函数对象。

于 2014-05-12T16:20:55.977 回答