为什么我们可以在`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 const
的std::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 int
( map
的key_type
)。 但是这个代码并不关心它是否不能移动key_type
。 它出于性能原因想要移动mapped_type
( std::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