在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
对象上操作,我们可以轻松找到错误,对吗?
答案 0 :(得分:91)
这里有一个你可以忽视的技巧,即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
请注意,您提到的代码中的错误是性能问题,而不是稳定性问题,因此这样的错误不会导致崩溃, 。它只会使用较慢的副本。此外,对于没有移动构造函数的非const对象也会出现这样的错误,因此仅添加const
重载不会捕获所有这些错误。我们可以检查是否能够从参数类型移动构造或移动赋值,但这会干扰假定以回退到复制构造函数的通用模板代码。
哎呀,也许有人希望能够从const CAT&&
构建,我该说他能做什么?
答案 1 :(得分:45)
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
。我们有std::move
的移动构造函数,它完全采用这种类型。
它被称为。
现在,这种奇怪的类型确实比您的提案修复的错误更为罕见。
但是,另一方面,现有的T
在通用代码中效果更好,您不知道您使用的类型是T const
还是{{1}}
答案 2 :(得分:19)
到目前为止,其余答案忽略的一个原因是通用代码在移动面前具有弹性的能力。例如,假设我想编写一个泛型函数,它将所有元素移出一种容器,以创建另一种具有相同值的容器:
template <class C1, class C2>
C1
move_each(C2&& c2)
{
return C1(std::make_move_iterator(c2.begin()),
std::make_move_iterator(c2.end()));
}
很酷,现在我可以相对有效地从vector<string>
创建deque<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
({{1} } key_type
)。但是,如果代码无法移动map
,则此代码无需关心。出于性能原因,它希望移动key_type
(mapped_type
)。
就这个例子而言,在通用编码中有无数其他例子,std::string
是移动请求,而不是要求移动。
答案 3 :(得分:2)
我和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()时,那么在使用单词移动时它实际上是误导。
如果语言鼓励使用std :: move的人提高效率,那么它必须尽早防止这样的陷阱,特别是对于这种明显的字面矛盾。
我同意人们应该意识到移动一个常数是不可能的,但这个义务并不意味着当明显的矛盾发生时编译器可以保持沉默。
<强> 2。该对象没有移动构造函数
就个人而言,我认为这是与OP关注的另一个故事,正如Chris Drew所说。
@hvd这对我来说似乎有点不争论。仅仅因为OP的建议没有解决世界上的所有错误并不一定意味着它是一个坏主意(它可能是,但不是因为你给出的原因)。 - 克里斯德鲁
答案 4 :(得分:1)
幸运的是,您可以使用clang-tidy的检查来发现此类问题:https://clang.llvm.org/extra/clang-tidy/checks/performance-move-const-arg.html
答案 5 :(得分:0)
我很惊讶没有人提到它的向后兼容性方面。我相信std::move
是专为C ++ 11而设计的。想象一下,您正在使用旧的代码库,该代码库高度依赖C ++ 98库,因此,如果不对副本分配进行回退,那么移动会造成麻烦。