为什么我们可以在const对象上使用std::move
Why can we use `std::move` on a `const` object?
在C++11中,我们可以编写以下代码:
struct Cat {
Cat(){}
};
const Cat cat;
std::move(cat); //this is valid in C++11
当我调用std::move
时,意味着我要移动对象,即我将更改对象。移动const
对象是不合理的,那么std::move
为什么不限制这种行为呢?这将是一个陷阱在未来,对吧?
这里陷阱的意思是布兰登在评论中提到的:
"我认为他是认真的,因为如果他不这样做,他就会偷偷地"诱捕"他意识到,他最终得到了一份不是他想要的副本。"
在Scott Meyers的《有效的现代C++》一书中,他举了一个例子:
class Annotation {
public:
explicit Annotation(const std::string text)
: value(std::move(text)) //here we want to call string(string&&),
//but because text is const,
//the return type of std::move(text) is const std::string&&
//so we actually called string(const string&)
//it is a bug which is very hard to find out
private:
std::string value;
};
如果std::move
被禁止在const
对象上操作,我们可以很容易地发现错误,对吧?
这里有一个技巧你忽略了,即std::move(cat)
实际上不会移动任何东西。它只是告诉编译器尝试移动。但是,由于您的类没有接受const CAT&&
的构造函数,因此它将使用隐式const CAT&
复制构造函数,并安全地进行复制。没有危险,就没有陷阱。如果复制构造函数由于任何原因被禁用,您将得到一个编译器错误。
struct CAT
{
CAT(){}
CAT(const CAT&) {std::cout << "COPY";}
CAT(CAT&&) {std::cout << "MOVE";}
};
int main() {
const CAT cat;
CAT cat2 = std::move(cat);
}
打印COPY
而不是MOVE
。
http://coliru.stacked-crooked.com/a/0dff72133dbf9d1f
请注意,您提到的代码中的错误是性能问题,而不是稳定性的问题,因此这样的错误永远不会导致崩溃。它只会使用较慢的副本。此外,对于没有move构造函数的非常量对象也会出现这样的错误,因此仅仅添加const
重载并不能捕获所有对象。我们可以检查从参数类型中移动构造或移动赋值的能力,但这会干扰假设依赖复制构造函数的通用模板代码。见鬼,也许有人想从const CAT&&
构建,我是谁说他做不到?
struct strange {
mutable size_t count = 0;
strange( strange const&& o ):count(o.count) { o.count = 0; }
};
const strange s;
strange s2 = std::move(s);
在这里我们看到std::move
在T const
上的使用。它返回一个T const&&
。我们有一个strange
的move构造函数,它正好采用这种类型。
它被称为。
现在,这种奇怪的类型确实比你的提案所修复的bug更罕见。
但是,另一方面,现有的std::move
在泛型代码中工作得更好,在泛型代码,您不知道使用的类型是T
还是T const
。
到目前为止,其他答案忽略的一个原因是通用代码在移动时具有弹性的能力。例如,我想写一个通用函数,将所有元素从一种容器中移出,以创建另一种具有相同值的容器:
template <class C1, class C2>
C1
move_each(C2&& c2)
{
return C1(std::make_move_iterator(c2.begin()),
std::make_move_iterator(c2.end()));
}
酷,现在我可以相对有效地从deque<string>
创建vector<string>
,并且在这个过程中每个单独的string
都会被移动。
但是,如果我想离开map
呢?
int
main()
{
std::map<int, std::string> m{{1, "one"}, {2, "two"}, {3, "three"}};
auto v = move_each<std::vector<std::pair<int, std::string>>>(m);
for (auto const& p : v)
std::cout << "{" << p.first << ", " << p.second << "} ";
std::cout << 'n';
}
如果std::move
坚持使用非const
参数,则move_each
的上述实例化将不会编译,因为它正试图移动const int
(map
的key_type
)。但是这个代码不在乎是否不能移动key_type
。出于性能原因,它希望移动mapped_type
(std::string
)。
正是在这个例子和无数其他类似的例子中,std::move
是一个移动的请求,而不是移动的请求。
我和OP有同样的担忧。
std::move不会移动对象,也不能保证对象是可移动的。那为什么叫搬家?
我认为不能移动可能是以下两种情况之一:
1.移动类型为常量
我们在语言中使用const关键字的原因是,我们希望编译器防止对定义为const的对象进行任何更改。举Scott Meyers的书中的例子:
class Annotation {
public:
explicit Annotation(const std::string text)
: value(std::move(text)) // "move" text into value; this code
{ … } // doesn't do what it seems to!
…
private:
std::string value;
};
它的字面意思是什么?将const字符串移到值成员中——至少,在阅读解释之前,我已经理解了这一点。
如果在调用std::move()时,该语言打算不执行move或不保证move适用,那么在使用单词move时,这实际上是一种误导。
如果该语言鼓励人们使用std::move来提高效率,那么它必须尽早防止这样的陷阱,尤其是对于这种明显的字面矛盾。
我同意人们应该意识到移动常量是不可能的,但这一义务不应意味着编译器在发生明显矛盾时可以保持沉默。
2.对象没有移动构造函数
就我个人而言,我认为这与OP的担忧是分开的,正如Chris Drew所说的
@hvd对我来说,这似乎有点没有争议。仅仅因为OP的建议没有修复世界上所有的错误并不一定意味着这是一个坏主意(可能是,但不是因为你给出的原因)Chris Drew
我很惊讶没有人提到这方面的向后兼容性。我相信std::move
是特意在C++11中设计的。想象一下,您使用的是一个严重依赖C++98库的遗留代码库,因此如果没有拷贝分配的回退,移动将破坏一切。
幸运的是,您可以使用clang整洁的检查来发现这样的问题:https://clang.llvm.org/extra/clang-tidy/checks/performance/move-const-arg.html
- Usages of std::move
- 在C++中对T*类型执行std::move的意外行为
- 关于std::move的使用,是否有编译警告
- 我应该实现右值推送功能吗?我应该使用std::move吗
- 通过实例理解std::move及其目的
- 返回一个带有 std::move 的对象并链接函数
- 当 std::move 与 C 样式数组或不移动对象时会发生什么
- std::move a const std::vector in a lambda capture
- "std::forward"和"std::move"真的不生成代码吗?
- 如何在没有 std::move 的情况下移动临时对象
- 关于在成员重载中使用 std::move() 的问题
- 显式清除 std::move 之后的源资源
- std::move 如何使原始变量的值无效?
- 复制elision、std::move和链式函数调用
- 如果真的需要std::move,我们应该什么时候声明右值refs
- 使用std::move将std::unique_ptr作为qt信号参数传递
- 无法使用带有 std::move 的自定义删除器插入 std::unique_ptr
- C++:我应该在 return 语句中显式使用 std::move() 来强制移动吗?
- 编译器是否足够聪明,以至于 std::move 变量超出范围?
- std::move() 或其在局部变量上的显式等价物可以允许 elision 吗?