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


113

在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对象进行操作,我们可以很容易地发现该错误,对吗?


2
但是尝试移动它。尝试更改其状态。std::move本身对对象没有任何作用。有人会说std::move名字不好。
juanchopanza 2015年

3
它实际上并没有移动任何东西。它所做的全部都转换为右值引用。try CAT cat2 = std::move(cat);,假设CAT支持常规的移动分配。
WhozCraig 2015年

11
std::move只是演员,它实际上并没有移动任何东西
Red

2
@WhozCraig:小心,因为您发布的代码会在没有警告的情况下进行编译和执行,因此会产生误导。
Mooing Duck 2015年

1
@MooingDuck从来没有说过不会编译。它仅由于启用了默认的copy-ctor才起作用。静噪,车轮掉落。
WhozCraig 2015年

Answers:


55
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,它正是采用这种类型。

它被称为。

现在,的确,这种奇怪的类型比您的建议所要解决的错误更罕见。

但是,另一方面,现有的std::move代码在通用代码中效果更好,在这种情况下,您不知道要使用的类型是a T还是a T const


3
+1作为第一个答案,实际上试图解释为什么你会打电话给std::move一个上const对象。
克里斯·德鲁

+1用于显示功能const T&&。这表示“我将使用rvalue-ref但我保证不会对其进行修改”类型的“ API协议”。我想,除了使用可变的之外,这并不常见。也许另一个用例是能够使用forward_as_tuple几乎所有东西,然后再使用它。
F佩雷拉

107

您忽略了一个窍门,即std::move(cat) 实际上并没有移动任何东西。它只是告诉编译器尝试移动。但是,由于您的类没有接受a的构造函数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&&,我是谁,他不能呢?


激动。值得一提的是,在用户定义的常规move-constructor或赋值运算符的定义之后,隐式删除了copy-ctor也会通过破坏的编译来演示这一点。好答案。
WhozCraig 2015年

还值得一提的是,需要非const左值的复制构造函数也无济于事。[class.copy]§8:“否则,隐式声明的副本构造函数的形式应为X::X(X&)
Deduplicator

9
我认为他不是用计算机/装配术语来形容“陷阱”。我认为他的意思是“诱捕”他偷偷摸摸的偷偷摸摸,因为如果他不了解,他最终会得到不是他想要的副本。我猜..
布兰登

也许您再获得1票赞成票,我获得4枚,获得金牌。;)
Yakk-Adam Nevraumont 2015年

该代码示例的高5位。很好地传达了观点。
布伦特编写代码

20

到目前为止,其余答案都被忽略的原因之一是通用代码在面对移动时具有弹性。例如,假设我想编写一个通用函数,该函数将所有元素移出一种容器,以创建具有相同值的另一种容器:

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都将在此过程中移动。

但是,如果我想离开a,该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 intkey_typemap)。但是此代码并不关心是否无法移动key_type。它想移动mapped_typestd::string由于性能原因,)。

在这个示例中,以及在通用编码中无数类似的其他示例,它std::move移动要求,而不是移动的要求。


2

我和OP一样担心。

std :: 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;
    };

它的字面意思是什么?将const字符串移动到value成员-至少,这是我在阅读说明之前的理解。

如果在调用std :: move()时该语言打算不移动或不保证移动适用,那么在使用单词移动时会产生误导。

如果该语言鼓励人们使用std :: move提高效率,则必须尽早防止此类陷阱,尤其是对于这种明显的文字矛盾。

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

2.对象没有移动构造函数

就个人而言,正如克里斯·德鲁(Chris Drew)所说,我认为这是与OP无关的故事

@hvd对我来说似乎有点不争辩。仅仅因为OP的建议不能解决世界上的所有错误,并不意味着这是一个坏主意(可能是,但不是出于您给出的原因)。–克里斯·德鲁


1

我感到惊讶的是,没有人提到它的向后兼容性方面。我相信,std::move是专门为C ++ 11设计的。想象一下,您正在使用旧的代码库,该代码库高度依赖C ++ 98库,因此,如果不对副本分配进行回退,那么移动会造成麻烦。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.