代码之家  ›  专栏  ›  技术社区  ›  kennyc

目标C中的rvo和move语义++

  •  2
  • kennyc  · 技术社区  · 6 年前

    DR:是吗? __block 属性上的 std::vector 预防目标C++中的RVO?

    现代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;
    }
    

    但是,如果 亚块 属性应用于第二个向量,然后当该向量返回时,似乎正在创建该向量的副本。下面是一个稍微做作的例子:

    - (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
    }
    

    有很多堆栈溢出问题明确指出您不需要使用 std::move 当返回一个向量时,因为这将阻止复制省略的发生。

    然而, this Stack Overflow question 声明有时确实需要显式使用 STD::移动 无法删除副本时。

    是使用 亚块 在objective-c++中,复制省略是不可能的,并且 STD::移动 应该改为使用?我的分析似乎证实了这一点,但我希望得到一个更权威的解释。

    (这是用C++ 17支持的XCODE 10)。

    1 回复  |  直到 6 年前
        1
  •  1
  •   pmdj    6 年前

    我不知道权威,但是 __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_after , logger 仍然基于堆栈。(0x7FFF地址)
    • 调度后 内部执行 Block_copy() 捕获的块 记录器 . 这意味着现在必须将logger变量移到堆中。由于它是C++对象,这意味着调用复制构造函数。
    • 事实上,之后 调度后 返回, &logger 现在计算新(堆)地址。
    • 当然,必须销毁原始堆栈实例。
    • 只有在捕获块被销毁后,才会销毁堆实例。

    所以A 亚块 “variable”实际上是一个更复杂的对象,可以在后台根据需要在内存中移动。

    如果你随后回来 记录器 my_function ,rvo是不可能的,因为(a)它现在住在堆上,而不是堆栈上,(b)返回时不复制将允许由块捕获的实例发生变异。

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