【问题标题】:Stealing resources from std::map's keys allowed?允许从 std::map 的密钥中窃取资源?
【发布时间】:2020-06-06 13:08:38
【问题描述】:

在 C++ 中,可以从地图中窃取我以后不再需要的资源吗?更准确地说,假设我有一个带有std::string 键的std::map,并且我想通过使用std::move 窃取maps 键的资源来构造一个向量。请注意,对密钥的这种写访问会破坏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: 

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

【问题讨论】:

  • 这个“偷”的目的是什么?从地图中删除元素?那么为什么不简单地这样做(从地图中删除元素)?此外,修改const 值是总是 UB。
  • 显然会导致严重的bug!
  • 不是直接回答您的问题,而是:如果您不返回向量而是返回一个范围或一对迭代器怎么办?那将完全避免复制。在任何情况下,您都需要基准来跟踪优化的进度,并需要一个分析器来查找热点。
  • @ALX23z 你有这个声明的来源吗?我无法想象复制指针比复制整个内存区域更昂贵。
  • @SebastianHoffmann 在最近的 CppCon 上提到过,但不确定是在哪个谈话上。问题是std::string 有短字符串优化。这意味着在复制和移动方面有一些重要的逻辑,而不仅仅是指针交换,而且大部分时间移动意味着复制 - 以免您处理相当长的字符串。无论如何,统计差异很小,通常它肯定会根据执行的字符串处理类型而有所不同。

标签: c++ move-semantics


【解决方案1】:

您正在做未定义的行为,使用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 不会抱怨,如果我不调用它的方法(我不这样做),也许内部顺序在它的析构函数中并不重要?
  • 地图的键被创建const。改变 const 对象是即时 UB,无论之后是否有任何实际访问它们。
  • 这个方法有两个问题:(1)我想提取所有元素,我不想通过键(低效率的查找)而是通过迭代器来提取。我已经看到这也是可能的,所以这很好。 (2) 如果我错了,请纠正我,但是提取所有元素会产生巨大的开销(在每次提取时重新平衡内部树结构)?
  • @phinz 正如你所看到的on cppreference extract 当使用迭代器作为参数时,具有摊销的常数复杂性。 一些开销是不可避免的,但它可能不够重要。如果您有此未涵盖的特殊要求,则需要实现自己的map 以满足这些要求。 std 容器适用于常见的通用应用程序。它们未针对特定用例进行优化。
  • @HTNW 您确定密钥是创建的const 吗?在这种情况下,您能否指出my argumentation 的错误之处。
【解决方案2】:

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

其他一些答案(phinz'sDeuchie's)认为密钥不能存储为const 对象,因为从映射中提取节点产生的节点句柄允许非const 访问钥匙。这个推论乍一看似乎是合理的,但是介绍extract 功能的论文P0083R3 有一个关于这个主题的专门部分使这个论点无效:

担忧

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

未定义的行为

这个提议中最困难的部分来自理论 透视是提取的元素保留其 const 的事实 键类型。这可以防止移出或更改它。解决 为此,我们提供了 key 访问器函数,它 提供对元素持有的键的非常量访问 节点句柄。 此功能需要实现“魔术”以确保 在存在编译器优化的情况下它可以正常工作。一 做到这一点的方法是使用 pair&lt;const key_type, mapped_type&gt; 的联合 和pair&lt;key_type, mapped_type&gt;。这些之间的转换可以 使用类似于 std::launder 提取和重新插入。

我们不认为这会带来任何技术或哲学问题 问题。标准库存在的原因之一是 编写客户端无法编写的不可移植和神奇的代码 可移植的 C++(例如&lt;atomic&gt;&lt;typeinfo&gt;&lt;type_traits&gt; 等)。 这只是另一个这样的例子。编译器所需的一切 供应商实现这个神奇之处在于他们不会利用 undefined 用于优化目的的联合行为——以及当前的编译器 已经承诺这一点(在某种程度上它被利用 的)。

这确实对客户端施加了限制,如果这些功能 被使用,std::pair 不能被专门化,使得pair<const key_type, mapped_type> 具有不同的布局 pair&lt;key_type, mapped_type&gt;。我们觉得任何人的可能性 实际上想要这样做实际上是零,并且在正式的 措辞我们限制这些对的任何专业化。

请注意,key 成员函数是唯一的地方 技巧是必要的,并且不改变容器或对 是必需的。

(强调我的)

【讨论】:

  • 这实际上是原始问题答案的一部分,但我只能接受一个。
【解决方案3】:

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

This 回答声称

换句话说,如果你修改一个原来的 const 对象,你会得到 UB,否则不会。

因此,当且仅当 string 对象 p.first 未被创建为 const 对象时,问题中的行 v.emplace_back(make_pair(std::move(const_cast&lt;string&amp;&gt;(p.first)),p.second)); 不会导致 UB。现在请注意, reference about extract 状态

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

因此,如果我 extract 对应于 pnode_handlep 将继续生活在其存储位置。但提取后,我可以move 离开p 的资源,如druckermanly's answer 的代码。这意味着 p 以及 string 对象 p.first 最初没有创建为 const 对象。

因此,我认为map的键的修改不会导致UB和Deuchie's answer,似乎@987654340的现在损坏的树结构(现在多个相同的空字符串键) @ 不会在析构函数中引入问题。因此,问题中的代码至少在存在 extract 方法的 C++17 中应该可以正常工作(以及关于保持有效的指针的声明)。

更新

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

【讨论】:

  • 对不起,phinz,你的答案是错误的。我会写一个答案来解释这一点 - 它与工会和std::launder有关。
【解决方案4】:

编辑:这个答案是错误的。善良的cmets已经指出了错误,但我没有删除它,因为它已在其他答案中引用。

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

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


一条评论让我想起了我忽略的一点,基本上他是对的,但有一点我不完全同意:const_cast 可能不是 UB。 const 只是编译器和我们之间的一个承诺。标记为const 的对象仍然是一个对象,与那些没有const 的对象一样,就它们的类型和二进制形式的表示而言。当const 被丢弃时,它应该表现得好像它是一个普通的可变类。关于move,如果你想使用它,你必须传递一个&amp;而不是const &amp;,所以我看到它不是一个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' 是空的,但显然我们破坏了 map 的有序性,因为空字符串应该重新定位到前面。如果我们试图插入、查找或删除地图的某些特定节点,可能会造成灾难。

无论如何,我们可能会同意,绕过其公共接口来操作任何类的内部数据绝不是一个好主意。 findinsertremove等函数的正确性依赖于内部数据结构的有序性,这就是为什么我们应该远离窥视的想法。

【讨论】:

  • "至于解构时会不会出事,是没有问题的。"技术上是正确的,因为未定义的行为(const 值的更改)发生得更早。但是,“move [函数] 确保在移动类后解构 [一个对象] 是安全的”这一论点不成立:您不能安全地从 const 对象/引用移动,因为这需要修改,const 会阻止。您可以尝试使用const_cast 来解决该限制,但此时您充其量只是深入研究特定于实现的行为,如果不是 UB。
  • @hoffmale 谢谢,我忽略了,犯了一个大错误。如果不是你指出来,我在这里的回答可能会误导其他人。实际上我应该说move 函数使用&amp; 而不是const&amp;,所以如果有人坚持要从地图中移出一个键,他必须使用const_cast
  • "被标记为 const 的对象仍然是一个对象,就其类型和二进制形式的表示而言,与那些没有 const 的对象相同" 不。 const 对象可以放入只读存储器中。此外, const 使编译器能够对代码进行推理并缓存值,而不是为多次读取生成代码(这会在性能方面产生很大差异)。所以const_cast引起的UB会很讨厌。它可能在大多数情况下都有效,但会以微妙的方式破坏您的代码。
  • 但是在这里我认为我们可以确定对象不会被放入只读内存,因为在extract 之后,我们可以从同一个对象移动,对吧? (见我的回答)
  • 对不起,您的回答中的分析是错误的。我会写一个答案来解释这个 - 它与工会和std::launder有关
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-01-04
  • 2019-01-22
  • 2013-11-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多