首页 > 解决方案 > Objective-C++ 中的 RVO 和移动语义

问题描述

TL;DR:Objective-C++ 中的__block属性是否会阻止 RVO?std::vector

Modern C++中,从函数返回向量的规范方法是仅按值返回,以便尽可能使用返回值优化。在 Objective-C++ 中,这似乎以相同的方式工作。

- (void)fetchPeople {
  std::vector<Person> people = [self readPeopleFromDatabase];
}

- (std::vector<Person>)readPeopleFromDatabase {
  std::vector<Person> people;

  people.emplace_back(...);
  people.emplace_back(...);

  // No copy is made here.
  return people;
}

但是,如果该__block属性应用于第二个向量,那么当它返回时,似乎正在创建该向量的副本。这是一个稍微做作的例子:

- (std::vector<Person>)readPeopleFromDatabase {
  // __block is needed to allow the vector to be modified.
  __block std::vector<Person> people;

  void (^block)() = ^ {
    people.emplace_back(...);
    people.emplace_back(...);
  };


  block();

  #if 1

  // This appears to require a copy.
  return people;

  #else

  // This does not require a copy.
  return std::move(people);

  #endif
}

有很多 Stack Overflow 问题明确指出在返回向量时不需要使用std::move,因为这将防止发生复制省略。

但是,这个 Stack Overflow 问题指出,确实,有时您确实需要在std::move无法进行复制省略时显式使用。

在Objective-C++中使用__block复制省略是不可能的,std::move应该使用它吗?我的分析似乎证实了这一点,但我希望有一个更权威的解释。

(这是在支持 C++17 的 Xcode 10 上。)

标签: objective-cc++11objective-c-blocksobjective-c++

解决方案


我不知道权威,但是一个__block变量专门设计为能够比它所在的范围更长久,并且包含特殊的运行时状态,可以跟踪它是堆栈支持还是堆支持。例如:

#include <iostream>
#include <dispatch/dispatch.h>

using std::cerr; using std::endl;
struct destruct_logger
{
    destruct_logger()
    {}
    destruct_logger(const destruct_logger& rhs)
    {
        cerr << "destruct_logger copy constructor: " << &rhs << " --> " << this << endl;
    }
  void dummy() {}
  ~destruct_logger()
    {
        cerr << "~destruct_logger on " << this << endl;
    }
};

void my_function()
{
    __block destruct_logger logger;

    cerr << "Calling dispatch_after, &logger = " << &logger << endl;
    dispatch_after(
      dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(),
        ^{
            cerr << "Block firing\n";
            logger.dummy();
        });
    cerr << "dispatch_after returned: &logger = " << &logger << endl;
}

int main(int argc, const char * argv[])
{
    my_function();
    cerr << "my_function() returned\n";
    dispatch_main();
    return 0;
}

如果我运行该代码,我会得到以下输出:

Calling dispatch_after, &logger = 0x7fff5fbff718
destruct_logger copy constructor: 0x7fff5fbff718 --> 0x100504700
dispatch_after returned: &logger = 0x100504700
~destruct_logger on 0x7fff5fbff718
my_function() returned
Block firing
~destruct_logger on 0x100504700

这里发生了很多事情:

  • 在我们调用之前dispatch_afterlogger仍然是基于堆栈的。(0x7fff…地址)
  • dispatch_after在内部执行Block_copy()捕获logger. 这意味着现在必须将记录器变量移动到堆中。由于它是一个 C++ 对象,这意味着调用了复制构造函数。
  • 事实上,在dispatch_after返回之后,&logger现在计算新的(堆)地址。
  • 当然必须销毁原始堆栈实例。
  • 只有在捕获块被销毁后,堆实例才会被销毁。

因此,__block“变量”实际上是一个更复杂的对象,可以在后台按需在内存中移动。

如果您随后logger从中返回my_function,RVO 将不可能,因为 (a) 它现在存在于堆上,而不是堆栈上,并且 (b) 在返回时不制作副本将允许块捕获的实例发生突变。

我想可能让它依赖于运行时状态 - 使用 RVO 内存进行堆栈支持,然后如果它被移动到堆中,则在函数返回时复制回返回值。但这会使操作块的函数复杂化,因为现在需要将支持状态与变量分开存储。这似乎也是过于复杂和令人惊讶的行为,所以我对__block变量不会发生 RVO 并不感到惊讶。


推荐阅读