编译器需要表现为从
vector
到
Foo
调用发生了复制。
如果编译器能够证明在抽象机器行为中没有可观测的副作用(不是在实际计算机中!)涉及将
std::vector
移动到
Foo
,则可以这样做。
在您上面的情况下,这种情况(移动没有抽象机器可见的副作用)是正确的;然而,编译器可能无法证明它。
复制
std::vector<T>
时可能会出现以下可观察行为:
- 调用元素的复制构造函数。使用
int
进行此操作是无法观察到的。
- 在不同时间调用默认的
std::allocator<>
。这会调用
::new
和
::delete
(可能
1)。在任何情况下,
::new
和
::delete
在上述程序中未被替换,因此您不能在标准下观察到它。
- 在不同对象上多次调用
T
的析构函数。使用
int
不可观察。
- 调用
Foo
后
vector
仍非空。没有人检查它,所以如果它为空,那么就好像它不存在一样。
- 指向外部向量元素的引用、指针或迭代器与内部的不同。未对
Foo
之外的向量元素采取任何引用、向量或指针。
虽然您可能会说“但是如果系统内存不足,向量很大,这不是可观察到的吗?”:
抽象机器没有“内存不足”条件,它只是有时因非约束性原因而分配失败(抛出
std::bad_alloc
)。它不失败是抽象机器的有效行为,通过不分配(实际)内存(在实际计算机上)而不失败也是有效的,只要内存的不存在没有可观察的副作用。
稍微玩具化一点的情况:
int main() {
int* x = new int[std::size_t(-1)];
delete[] x;
}
虽然该程序明显分配了过多的内存,但编译器可以自由选择不分配任何内存。
我们可以更进一步。即使:
int main() {
int* x = new int[std::size_t(-1)];
x[std::size_t(-2)] = 2;
std::cout << x[std::size_t(-2)] << '\n';
delete[] x;
}
可以转化为
std::cout << 2 << '\n';
。那个大缓冲区必须存在于抽象状态下,但只要你的“真实”程序像抽象机器一样运行,它实际上不必分配它。
不幸的是,在任何合理的规模下这样做都很困难。C++程序中有很多信息泄漏的方式。因此,依靠这样的优化(即使发生了)也不会有好的结果。
重要的事实是,即使没有调用
std::move
,编译器也不需要像复制一样运行的情况是存在的。
当您在一个看起来像
return X;
的行中从函数返回本地变量,并且
X
是标识符,并且该本地变量具有自动存储期(在堆栈上),操作隐式移动,并且编译器(如果可以)可以将返回值和本地变量的存在省略为一个对象(甚至省略
move
)。
当您从临时对象构造对象时,情况也是如此-操作隐式移动(因为它绑定到rvalue)并且可以完全省略移动。
在这两种情况下,编译器必须将其视为移动(而不是复制),并且它可以省略移动操作。
std::vector<int> foo() {
std::vector<int> x = {1,2,3,4};
return x;
}
这段代码中的x
没有使用std::move
,但它被移动到返回值中,并且可以省略这个移动操作(x
和返回值可以合并为一个对象)。
示例代码:
std::vector<int> foo() {
std::vector<int> x = {1,2,3,4};
return std::move(x);
}
块省略也是如此:
std::vector<int> foo(std::vector<int> x) {
return x;
}
我们甚至可以阻止这个动作:
std::vector<int> foo() {
std::vector<int> x = {1,2,3,4};
return (std::vector<int> const&)x;
}
甚至更多:
std::vector<int> foo() {
std::vector<int> x = {1,2,3,4};
return 0,x;
}
由于隐式移动规则是故意脆弱的。(
0,x
是备受诟病的
,
运算符的用法之一。)
现在,不建议依赖于像最后一个基于
,
的例子中不发生隐式移动的情况:自从将隐式移动添加到语言中以来,标准委员会已经将一个隐式复制案例更改为隐式移动,因为他们认为它是无害的(其中函数返回具有
A(B&&)
构造函数的类型
A
,并且返回语句是
return b;
,其中
b
的类型为
B
;在C++11发布时进行了复制,现在进行了移动。) 隐式移动的进一步扩展不能被排除:明确地转换为
const&
可能是现在和未来防止它的最可靠方法。
::operator new
可以在不同的翻译单元中被替换,然后链接在一起,因此在没有整个程序知识的情况下省略调用是棘手的。此外,合并规则适用于新表达式,而不是对::operator new
的直接调用,这就是std::allocator
所做的。 - T.C.