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


15

在C ++中,可以从以后不再需要的映射中窃取资源了吗?更准确地说,假设我有一个std::mapwith std::string键,并且我想通过使用窃取maps键的资源来构造一个向量std::move。请注意,对键的这种写访问会破坏的内部数据结构(键的顺序),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: 

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


3
这种“偷窃”的目的是什么?要从地图上删除元素?那为什么不简单地做到这一点(擦除地图中的元素)呢?同样,修改const始终是UB。
一些程序员伙计

显然会导致严重的错误!
rezaebrh

1
不是您问题的直接答案,而是:如果您不返回向量而是一个范围或一对迭代器,该怎么办?这样可以避免完全复制。无论如何,您都需要基准来跟踪优化进度,并且需要使用探查器来查找热点。
乌尔里希·埃克哈特

1
@ ALX23z您是否有该声明的任何信息来源。我无法想象复制指针比复制整个内存区域要昂贵得多。
塞巴斯蒂安·霍夫曼

1
@SebastianHoffmann在最近的CppCon上被提及,不确定是哪个话题。事情是std::string具有短字符串优化。这意味着在复制和移动时存在一些不平凡的逻辑,而不仅仅是指针交换,而且在大多数情况下,移动还意味着复制-以免您处理相当长的字符串。无论如何,统计差异很小,并且总的来说,它肯定会根据执行的字符串处理类型而有所不同。
ALX23z

Answers:


18

您正在执行未定义的行为,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;。:)


谢谢,我会尝试的!但是,您确定我不能像以前那样做吗?我的意思是,map如果不调用它的方法(我不这样做),就不会抱怨,也许内部顺序在其析构函数中并不重要?
菲茨

2
地图的键已创建const。突变const对象是即时UB,无论之后是否有任何实际访问它们。
HTNW

此方法有两个问题:(1)因为我想提取所有元素,所以我不想通过键(效率低的查找)而是通过迭代器提取。我已经看到这也是可能的,所以很好。(2)如果我错了,请纠正我,但是要提取所有元素会产生巨大的开销(每次提取时都要重新平衡内部树结构)?
菲茨

2
@phinz如您在使用迭代器作为参数时在cppreference上 看到的extract那样,摊销了恒定的复杂性。不可避免地会产生一些开销,但是它可能并不重要。如果您对此有特殊的要求,那么您将需要自己实现map满足这些要求的条件。这些std容器仅适用于一般通用应用程序,而没有针对特定用例进行优化。
核桃

@HTNW您确定已创建密钥const吗?在这种情况下,请您指出我的论点错了。
菲茨

4

您的代码尝试修改const对象,因此它具有未定义的行为,正如druckermanly的答案正确指出的那样。

其他一些答案(phinzDeuchie的观点)则认为,不得将密钥存储为const对象,因为从映射中提取节点而导致的节点句柄允许不const访问密钥。最初,这种推断似乎是合理的,但是引入功能的论文P0083R3extract对此主题有专门的章节,使该论点无效:

顾虑

有人对此设计提出了一些担忧。我们将在这里解决。

未定义的行为

从理论角度来看,该建议最困难的部分是提取的元素保留其const键类型的事实。这样可以防止移出或更改它。为了解决这个问题,我们提供了密钥访问器功能,该功能提供了对由节点句柄持有的元素中的密钥的非常量访问。此功能需要实现“魔术”,以确保在存在编译器优化的情况下其正确运行。一种实现方法是使用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>。我们认为任何人实际想要执行此操作的可能性实际上为零,并且在正式措辞中,我们限制了这些对的任何专门化。

请注意,关键成员函数是唯一需要这种技巧的地方,并且不需要更改容器或容器对。

(强调我的)


这实际上是原始问题答案的一部分,但我只能接受一个。
菲茨

0

我不认为const_cast在这种情况下,修改会导致未定义的行为,但请评论此论点是否正确。

这个答案声称

换句话说,如果您修改了原始的const对象,您将获得UB,否则,您将得到UB。

因此v.emplace_back(make_pair(std::move(const_cast<string&>(p.first)),p.second));,仅当未将string对象p.first创建为const对象时,问题中的行才不会导致UB 。现在请注意有关extract状态的 参考

提取节点会使迭代器对提取的元素无效。指向提取的元素的指针和引用仍然有效,但是在元素由节点句柄拥有时不能使用:如果将元素插入容器中,则它们变得可用。

因此,如果我extractnode_handle相对应p,则p继续住在其存储位置。但是提取后,我被允许像德鲁克曼利的答案代码中那样浪费move资源。这意味着该对象也因此最初未被创建为const对象。ppstringp.first

因此,我认为对map'键的修改不会导致UB,并且从Deuchie的回答来看,似乎现在损坏的树结构(现在有多个相同的空字符串键)map也不会在析构函数中引入问题。因此,问题中的代码至少应在extract存在该方法的C ++ 17中正常工作(并且有关指针的声明仍然有效)。

更新资料

我现在认为这个答案是错误的。我没有删除它,因为其他答案引用了它。


1
抱歉,菲恩斯,你的回答是错误的。我将写一个答案来解释这一点-它与工会和工会有关std::launder
LF

-1

编辑:这个答案是错误的。友善的评论指出了这些错误,但由于它已在其他答案中引用,因此我没有将其删除。

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

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


一条评论让我想起了我所忽略的观点,基本上他是对的,但有一点我并不完全同意:const_cast可能不是UB。const只是我们和编译器之间的承诺。就其类型和二进制形式的表示而言,标记为const的对象仍然与不使用的对象相同const。当const被抛弃,它应该表现为,如果它是一个正常的可变类。关于move,如果要使用它,则必须传递a &而不是a 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
“关于在解构时是否会引起麻烦,这不是问题。” 从技术上讲是正确的,因为未定义的行为(更改const值)较早发生。但是,参数“ move[函数]确保在移动后安全地解构类的[对象]”并不成立:您无法安全地从const对象/引用中移出,因为这需要进行修改,因此const防止。您可以尝试使用来解决该限制const_cast,但是到那时,您最好深入了解特定于实现的行为(如果不是UB)。
hoffmale

@hoffmale谢谢,我忽略了一个大错误。如果不是您指出这一点,我在这里的回答可能会误导别人。实际上,我应该说该move函数使用a &而不是a const&,因此如果有人坚持认为他将键从地图中移出,则必须使用const_cast
Deuchie

1
“在类型和二进制表示形式方面,标为const的对象仍然与不包含const的对象相同,仍然是对象。”否const对象可以放在只读存储器中。同样,const使编译器能够推理代码并缓存值,而不是为多次读取生成代码(这可能会在性能方面产生很大差异)。因此,由引起的UB const_cast将令人讨厌。它可能在大多数时间都有效,但是会以微妙的方式破坏代码。
LF

但是在这里,我认为我们可以确保不将对象放入只读存储器,因为在之后extract,我们可以从同一对象移动,对吗?(请参阅我的回答)
phinz

1
抱歉,您答案中的分析是错误的。我将写一个答案来解释这一点-它与工会有关,并且std::launder
LF
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.