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

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

本文关键字:std move 对象 我们 const 为什么      更新时间:2023-10-16

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

这里有一个技巧你忽略了,即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

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

酷,现在我可以相对有效地从deque<string>创建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 intmapkey_type)。但是这个代码不在乎是否不能移动key_type。出于性能原因,它希望移动mapped_typestd::string)。

正是在这个例子和无数其他类似的例子中,std::move是一个移动的请求,而不是移动的请求。

我和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()时,该语言打算不执行move或不保证move适用,那么在使用单词move时,这实际上是一种误导。

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

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

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

就我个人而言,我认为这与OP的担忧是分开的,正如Chris Drew所说的

@hvd对我来说,这似乎有点没有争议。仅仅因为OP的建议没有修复世界上所有的错误并不一定意味着这是一个坏主意(可能是,但不是因为你给出的原因)Chris Drew

我很惊讶没有人提到这方面的向后兼容性。我相信std::move是特意在C++11中设计的。想象一下,您使用的是一个严重依赖C++98库的遗留代码库,因此如果没有拷贝分配的回退,移动将破坏一切。

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