为什么我们可以在`const`对象上使用`std :: move`?

在C ++ 11中,我们可以编写这样的代码:

struct Cat { Cat(){} }; const Cat cat; std::move(cat); //this is valid in C++11 

当我打电话std::move ,这意味着我想要移动的对象,即我将改变对象。 移动一个const对象是不合理的,为什么std::move不会限制这个行为? 这将是一个陷阱,对吧?

这里的陷阱就像布兰登在评论中提到的那样:

“我认为他的意思是”欺骗“他偷偷摸摸的偷偷摸摸,因为如果他没有意识到的话,他最终会得到一个不是他想要的副本。

在Scott Meyers的“Effective Modern 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对象上运行,我们可以很容易的find这个bug,对吧?

 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); 

在这里我们看到在T conststd::move上使用。 它返回一个T const&& 。 我们有一个strange的移动构造函数,完全采用这种types。

它被称为。

现在,这种奇怪的types确实比您的提案可以解决的错误更为罕见。

但是,另一方面,现有的std::move在通用代码中工作得更好,在这种代码中,您不知道所使用的types是T还是T const

这里有一个你可以忽略的技巧,即std::move(cat) 实际上并没有移动任何东西 。 它只是告诉编译器试图移动。 但是,由于你的类没有接受const CAT&&构造const CAT&& ,它将使用隐式const CAT& copy构造函数,并安全地复制。 没有危险,没有陷阱。 如果复制构造函数因任何原因被禁用,您将得到一个编译器错误。

 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

请注意,您提到的代码中的错误是性能问题,而不是稳定性问题,所以这样的错误不会导致崩溃。 它只会使用较慢的副本。 此外,对于没有移动构造函数的非常量对象也会出现这样的错误,所以仅仅添加一个const重载就不能捕获所有这些错误。 我们可以检查移动构造的能力,或者移动参数types的赋值,但是这会干扰应该在复制构造函数上回落的通用模板代码。 而且,也许有人希望能够从const CAT&&构build,我可以说他不能?

到目前为止,其他答案都忽略了的一个原因就是generics代码在移动时有弹性。 例如,假设我想写一个通用函数,将所有元素从一种容器中移出,以创build另一种具有相同值的容器:

 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>创build一个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 intmapkey_type )。 但是这个代码并不关心它是否不能移动key_type 。 它出于性能原因想要移动mapped_typestd::string )。

对于这个例子以及无数的其他例子,在generics编码中, std::move是一个移动请求 ,而不是移动请求

我有和OP一样的担心。

std :: move不移动对象,既不保证对象是可移动的。 那为什么叫做移动?

我认为不能移动可以是以下两种情况之一:

1.移动types是const。

我们在该语言中使用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; }; 

这是什么意思? 将一个常量string移到值成员 – 至less,这是我的理解,然后我阅读解释。

如果语言不打算移动或不保证移动适用于std :: move()被调用时,那么在使用移动字符时,字面上是误导性的。

如果语言鼓励人们使用std :: move来获得更好的效率,就必须尽早地避免这种陷阱,特别是对于这种明显的字面上的矛盾。

我同意人们应该意识到,一个常数是不可能的,但是这个义务并不意味着当出现明显的矛盾时编译器可以保持沉默。

2.对象没有移动构造器

就我个人而言,我认为这与OP的担忧是分开的,正如Chris Drew所说

@ hvd对我来说这似乎有点不争论。 只是因为OP的build议并不能解决世界上所有的错误,并不一定意味着这是一个坏主意(这可能是,但不是因为你给的原因)。 – Chris Drew