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

kzipqqlq  于 2023-01-03  发布在  其他
关注(0)|答案(6)|浏览(285)

在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对象上操作,我们就可以很容易地找到bug,对吗?

798qvoo8

798qvoo81#

这里有一个你忽略了的技巧,那就是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永远不会导致崩溃。它只会使用一个更慢的副本。另外,这样的bug也会发生在没有move构造函数的非常数对象上。所以仅仅添加一个const重载并不能捕获所有的变量。我们可以检查从参数类型中移动construct或assign的能力,但这会干扰泛型模板代码,而泛型模板代码应该依赖于复制构造函数。见鬼,也许有人希望能够从const CAT&&构造,我凭什么说他不能?

uwopmtnx

uwopmtnx2#

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

kmbjn2e3

kmbjn2e33#

到目前为止,其余的答案都忽略了一个原因,那就是 * 泛型 * 代码在面对移动时具有弹性的能力,比如说,我想写一个泛型函数,将一种容器中的所有元素都移出,以创建另一种具有相同值的容器:

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是一个 * 移动请求 *,而不是移动需求。

7ajki6be

7ajki6be4#

我和观察员有同样的担心。
std::move不移动对象,也不保证对象是可移动的,那为什么叫move呢?
我认为不可移动可能是以下两种情况之一:

1.移动类型为常量。

我们在语言中使用const关键字的原因是我们希望编译器阻止对定义为const的对象的任何更改。

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()时,语言不打算执行move或不保证move是适用的,那么在使用单词move时,从字面上看就是误导。
如果语言鼓励人们使用std::move来获得更好的效率,那么它必须尽早防止类似的陷阱,尤其是对于这种明显的字面矛盾。
我同意人们应该意识到移动常量是不可能的,但是这个义务不应该意味着当明显的矛盾发生时编译器可以保持沉默。

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

就我个人而言,我认为这是一个独立的故事从OP的关注,因为克里斯德鲁说
@hvd这对我来说似乎是一个没有争议的问题。仅仅因为OP的建议没有修复世界上所有的bug并不一定意味着它是一个坏主意(它可能是,但不是你给予的原因)。- Chris Drew

bvpmtnay

bvpmtnay5#

我很惊讶没有人提到向后兼容性方面。我相信,std::move是专门为C11设计的。想象一下,您正在使用一个遗留的代码库,它严重依赖于C98库,因此如果没有副本分配的后备,移动将破坏一切。

6ss1mwsb

6ss1mwsb6#

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

相关问题