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

Why can we use `std::move` on a `const` object?

在C ++ 11中,我们可以编写以下代码:

1
2
3
4
5
6
struct Cat {
   Cat(){}
};

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

当我调用std::move时,这意味着我想移动对象,即我将更改对象。 移动const对象是不合理的,那么为什么std::move不限制这种行为? 将来会成为陷阱,对吗?

陷阱的含义如布兰登在评论中所述:

" I think he means it"traps" him sneaky sneaky because if he doesn't
realize, he ends up with a copy which is not what he intended."

在斯科特·迈耶斯(Scott Meyers)的著作《有效的现代C ++》中,他举了一个例子:

1
2
3
4
5
6
7
8
9
10
11
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对象进行操作,我们可以很容易地发现该错误,对吗?


您忽略了一个技巧,即std::move(cat)实际上并没有移动任何东西。它只是告诉编译器尝试移动。但是,由于您的类没有接受const CAT&&的构造函数,因此它将使用隐式const CAT&复制构造函数,并安全地进行复制。没有危险,没有陷阱。如果出于任何原因禁用了复制构造函数,则会出现编译器错误。

1
2
3
4
5
6
7
8
9
10
11
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
2
3
4
5
6
7
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的move构造函数,它正是采用这种类型。

它被称为。

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

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


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

1
2
3
4
5
6
7
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创建一个vector,并且每个string都将在此过程中移动。

但是,如果我想从map移走怎么办?

1
2
3
4
5
6
7
8
9
10
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 << '
'
;
}

如果std::move坚持使用非const参数,则move_each的上述实例将无法编译,因为它试图移动const int(mapkey_type)。但是此代码不在乎是否不能移动key_type。由于性能原因,它希望移动mapped_type(std::string)。

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


我和OP一样担心。

std :: move不移动对象,也不保证对象可移动。那为什么叫移动呢?

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

1.移动类型为const。

我们在语言中使用const关键字的原因是我们希望编译器阻止对定义为const的对象进行任何更改。给出斯科特·迈耶斯(Scott Meyers)书中的示例:

1
2
3
4
5
6
7
8
9
    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 That seems like a bit of a non-argument to me. Just because OP's suggestion doesn't fix all bugs in the world doesn't necessarily mean it is a bad idea (it probably is, but not for the reason you give). – Chris Drew


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