Objective-C++ 中的 RVO 和移动语义
RVO and Move Semantics in Objective-C++
TL;DR:std::vector
上的 __block
属性是否可以防止 Objective-C++ 中的 RVO?
在 现代 C++ 中,从函数 return 向量的规范方法是 return 它的值,以便 return 如果可能,可以使用值优化。在 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
属性应用到第二个矢量,则在 return 时似乎正在创建该矢量的副本。这是一个稍微做作的例子:
- (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 问题明确指出在 returning 向量时不需要使用 std::move
,因为这会阻止复制省略的发生。
但是,this Stack Overflow question 指出,确实有些时候您确实需要明确使用 std::move
,而复制省略是不可能的。
在 Objective-C++ 中使用 __block
是不可能进行复制省略且应该使用 std::move
的情况之一吗?我的分析似乎证实了这一点,但我想要更权威的解释。
(在 Xcode 10 上支持 C++17。)
我不知道权威性如何,但是 __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…地址)
dispatch_after
在内部执行捕获 logger
的块的 Block_copy()
。这意味着现在必须将记录器变量移动到堆中。因为它是一个 C++ 对象,这意味着复制构造函数被调用。
- 事实上,在
dispatch_after
return 秒之后,&logger
现在评估为新的(堆)地址。
- 原来的栈实例当然要销毁了。
- 只有在捕获块被销毁后,堆实例才会被销毁。
所以 __block
"variable" 实际上是一个更复杂的对象,可以在后台根据需要在内存中移动。
如果您随后要从 my_function
return logger
,RVO 将不可能,因为 (a) 它现在存在于堆上,而不是堆栈上,并且(b) 不在 returning 上制作副本将允许块捕获的实例发生变异。
我猜想可以让它 运行 依赖于时间状态 - 使用 RVO 内存进行堆栈支持,然后如果它被移动到堆中,则复制回 return 值当函数 returns.但这会使操作块的函数复杂化,因为支持状态现在需要与变量分开存储。这似乎也过于复杂和令人惊讶的行为,所以我对 __block
变量没有发生 RVO 并不感到惊讶。
TL;DR:std::vector
上的 __block
属性是否可以防止 Objective-C++ 中的 RVO?
在 现代 C++ 中,从函数 return 向量的规范方法是 return 它的值,以便 return 如果可能,可以使用值优化。在 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
属性应用到第二个矢量,则在 return 时似乎正在创建该矢量的副本。这是一个稍微做作的例子:
- (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 问题明确指出在 returning 向量时不需要使用 std::move
,因为这会阻止复制省略的发生。
但是,this Stack Overflow question 指出,确实有些时候您确实需要明确使用 std::move
,而复制省略是不可能的。
在 Objective-C++ 中使用 __block
是不可能进行复制省略且应该使用 std::move
的情况之一吗?我的分析似乎证实了这一点,但我想要更权威的解释。
(在 Xcode 10 上支持 C++17。)
我不知道权威性如何,但是 __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…地址) dispatch_after
在内部执行捕获logger
的块的Block_copy()
。这意味着现在必须将记录器变量移动到堆中。因为它是一个 C++ 对象,这意味着复制构造函数被调用。- 事实上,在
dispatch_after
return 秒之后,&logger
现在评估为新的(堆)地址。 - 原来的栈实例当然要销毁了。
- 只有在捕获块被销毁后,堆实例才会被销毁。
所以 __block
"variable" 实际上是一个更复杂的对象,可以在后台根据需要在内存中移动。
如果您随后要从 my_function
return logger
,RVO 将不可能,因为 (a) 它现在存在于堆上,而不是堆栈上,并且(b) 不在 returning 上制作副本将允许块捕获的实例发生变异。
我猜想可以让它 运行 依赖于时间状态 - 使用 RVO 内存进行堆栈支持,然后如果它被移动到堆中,则复制回 return 值当函数 returns.但这会使操作块的函数复杂化,因为支持状态现在需要与变量分开存储。这似乎也过于复杂和令人惊讶的行为,所以我对 __block
变量没有发生 RVO 并不感到惊讶。