为什么我们可以在‘ 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 的《有效的现代 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对象上操作,我们可以很容易地发现错误,对不对?

52580 次浏览

你忽略了一个技巧,那就是 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

请注意,您提到的代码中的 bug 是 表演问题,而不是 稳定问题,所以这样的 bug 永远不会导致崩溃。它只是使用一个较慢的副本。此外,对于没有 move 构造函数的非常量对象也会出现这样的 bug,因此仅仅添加一个 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::moveT 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()));
}

酷,现在我可以相对有效地创建一个 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(mapkey_type)。但是这个代码 不在乎如果它不能移动 key_type。出于性能方面的考虑,它希望移动 mapped_type(std::string)。

正是对于这个例子,以及在通用编码中无数其他类似的例子,std::move请求移动,而不是需要移动的。

我和观察所有同样的担心。

Move 不移动对象,也不保证对象是可移动的。那为什么叫 move?

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

1. 移动类型为 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;
};

字面意思是什么?将一个常量字符串移动到值成员-至少,这是我在阅读解释之前的理解。

如果语言打算不做 move 或者不保证 move 在调用 std: : move ()时适用,那么在使用单词 move 时就会产生字面上的误导。

如果语言鼓励人们使用 std: : move 来获得更好的效率,那么它必须尽早防止这样的陷阱,特别是对于这种明显的字面矛盾。

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

2. 对象没有 move 构造函数

就个人而言,我认为这是一个独立的故事,从 OP 的关注,正如克里斯德鲁说

@ hvd 这对我来说似乎没什么好争论的。仅仅因为 OP 的建议不能修复世界上所有的 bug 并不意味着它是一个坏主意(它可能是,但不是你给出的理由)。Chris Drew

我很惊讶居然没人提到这件事的向下兼容。我相信,std::move是特意在 C + + 11中设计的。假设您正在使用一个遗留代码库,该代码库严重依赖于 C + + 98库,因此如果没有后备的复制分配,移动将会破坏一些东西。

幸运的是,您可以使用 clang-clean 检查来找到这样的问题: https://clang.llvm.org/extra/clang-tidy/checks/performance-move-const-arg.html