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

时间:2015-02-18 22:22:46

标签: c++ c++11

在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::moveconst对象上操作,我们可以轻松找到错误,对吗?

6 个答案:

答案 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_typemapped_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库,因此,如果不对副本分配进行回退,那么移动会造成麻烦。