是否允许从std :: map的键中窃取资源?

问题描述 投票:9回答:4

在C ++中,可以从以后不再需要的地图中窃取资源了吗?更准确地说,假设我有一个带有std::map键的std::string,并且我想通过使用map窃取std::move s键的资源来构造一个向量。请注意,对键的这种写访问会破坏map的内部数据结构(键的顺序),但此后我将不再使用它。

问题:我可以这样做没有任何问题,还是会导致意外的错误,例如在map的析构函数中,因为我以不打算使用std::map的方式访问它?

这里是一个示例程序:

#include<map>
#include<string>
#include<vector>
#include<iostream>
using namespace std;
int main(int argc, char *argv[])
{
    std::vector<std::pair<std::string,double>> v;
    { // new scope to make clear that m is not needed 
      // after the resources were stolen
        std::map<std::string,double> m;
        m["aLongString"]=1.0;
        m["anotherLongString"]=2.0;
        //
        // now steal resources
        for (auto &p : m) {
            // according to my IDE, p has type 
            // std::pair<const class std::__cxx11::basic_string<char>, double>&
            cout<<"key before stealing: "<<p.first<<endl;
            v.emplace_back(make_pair(std::move(const_cast<string&>(p.first)),p.second));
            cout<<"key after stealing: "<<p.first<<endl;
        }
    }
    // now use v
    return 0;
}

它产生输出:

key before stealing: aLongString
key after stealing: 
key before stealing: anotherLongString
key after stealing: 

编辑:我想对大型地图的整个内容执行此操作,并通过窃取资源来保存动态分配。

c++ move-semantics
4个回答
16
投票

您正在执行未定义的行为,请使用const_cast修改const变量。不要那样做之所以是const,是因为地图是按其键排序的。因此,就地修改键会破坏构建地图的基础假设。

您永远不要使用const_cast从变量中删除const 修改该变量。

话虽如此,C ++ 17为您的问题提供了解决方案:std::mapextract函数:

#include <map>
#include <string>
#include <vector>
#include <utility>

int main() {
  std::vector<std::pair<std::string, double>> v;
  std::map<std::string, double> m{{"aLongString", 1.0},
                                  {"anotherLongString", 2.0}};

  auto extracted_value = m.extract("aLongString");
  v.emplace_back(std::make_pair(std::move(extracted_value.key()),
                                std::move(extracted_value.mapped())));

  extracted_value = m.extract("anotherLongString");
  v.emplace_back(std::make_pair(std::move(extracted_value.key()),
                                std::move(extracted_value.mapped())));
}

而且不要using namespace std;。 :)


2
投票

您的代码尝试修改const对象,因此它具有未定义的行为,因为druckermanly's answer正确指出。

[其他一些答案(phinz'sDeuchie's)认为,密钥不能存储为const对象,因为从映射中提取节点导致的节点句柄允许非const访问密钥。最初,这种推断似乎是合理的,但是P0083R3是介绍extract功能的论文),在该主题的专用部分中,该论点无效:

关注

对此设计提出了若干关注。我们将解决他们在这里。

未定义的行为

[从理论上讲,该提案最困难的部分透视是提取的元素保留其const的事实密钥类型。这样可以防止移出或更改它。解决为此,我们提供了key访问器函数,该函数提供对对象所持有元素中密钥的非常量访问节点句柄。 此功能需要实施“魔术”以确保它在存在编译器优化的情况下可以正常工作。一方法是通过pair<const key_type, mapped_type>的并集和pair<key_type, mapped_type>。这些之间可以转换使用类似于std::launder关于提取和重新插入。

我们认为这不构成任何技术性或哲学性问题。标准库存在的原因之一是编写客户端无法写入的不可移植的神奇代码可移植的C ++(例如<atomic><typeinfo><type_traits>等)。这只是另一个这样的例子。编译器所需的全部供应商实施这种魔力在于他们不会利用未定义的出于优化目的而在联合中的行为-当前是编译器已经承诺(在一定程度上被利用)这里)。

这确实限制了客户端,如果这些功能使用std::pair时,不能将其专门化,因此pair<const key_type, mapped_type>的布局与pair<key_type, mapped_type>。我们感到任何人的可能性实际上要这样做实际上是零,在形式上用语上,我们限制了这些对的任何专业化。

注意,key成员函数是唯一这样的位置技巧是必要的,并且无需更改容器或容器对是必需的。

(重点是我的)


-2
投票

@@ druckermanly回答了您的第一个问题,该问题表示,强行更改map中的键会破坏构建map内部数据结构的顺序(红黑树)。但是使用extract方法是安全的,因为它有两件事:将键移出地图,然后将其删除,因此它根本不会影响地图的有序性。

您提出的关于在解构时是否会引起麻烦的另一个问题不是问题。映射解构时,它将调用其每个元素的解构器(mapped_types等),move方法可确保在移动类后对其进行解构是安全的。所以不用担心简而言之,move的操作可确保安全删除或将某些新值分配给“移动”类。专门针对stringmove方法可以将其char指针设置为nullptr,因此它不会删除在调用原始类的解构函数时移动的实际数据。


[评论使我想起了我所忽略的观点,基本上他是对的,但是有一点我并不完全同意:const_cast可能不是UB。 const只是编译器和我们之间的承诺。就其类型和二进制形式的表示而言,标为const的对象仍然与不包含const的对象相同。丢弃const时,其行为应类似于普通的可变类。关于move,如果要使用它,则必须传递&而不是const &,因此,我认为它不是UB,它只是违反了const的承诺而已数据移开。

[我也分别使用MSVC 14.24.28314和Clang 9.0.0进行了两个实验,它们产生了相同的结果。

map<string, int> m;
m.insert({ "test", 2 });
m.insert({ "this should be behind the 'test' string.", 3 });
m.insert({ "and this should be in front of the 'test' string.", 1 });
string let_me_USE_IT = std::move(const_cast<string&>(m.find("test")->first));
cout << let_me_USE_IT << '\n';
for (auto const& i : m) {
    cout << i.first << ' ' << i.second << '\n';
}

输出:

test
and this should be in front of the 'test' string. 1
 2
this should be behind the 'test' string. 3

我们可以看到字符串'2'现在为空,但是显然我们已经破坏了地图的有序性,因为应该将空字符串重新放置在前面。如果我们尝试插入,查找或删除地图的某些特定节点,则可能会导致灾难。

无论如何,我们可能会同意,绕过它们的公共接口来操纵任何类的内部数据绝不是一个好主意。 findinsertremove函数等的正确性取决于内部数据结构的有序性,因此这就是我们不应该偷看内部的原因。


-2
投票

编辑:这个答案是错误的。

© www.soinside.com 2019 - 2024. All rights reserved.