【问题标题】:Is it valid to use std::transform with std::back_inserter?将 std::transform 与 std::back_inserter 一起使用是否有效?
【发布时间】:2020-04-04 15:21:35
【问题描述】:

Cppreference 有这个std::transform 的示例代码:

std::vector<std::size_t> ordinals;
std::transform(s.begin(), s.end(), std::back_inserter(ordinals),
               [](unsigned char c) -> std::size_t { return c; });

但它也说:

std::transform 不保证unary_opbinary_op 的按顺序应用。要将函数按顺序应用于序列或应用修改序列元素的函数,请使用std::for_each

这大概是为了允许并行实现。但是std::transform 的第三个参数是LegacyOutputIterator,它对++r 具有以下后置条件:

在此操作之后,r 不需要是可递增的,并且 r 的先前值的任何副本都不再需要是可取消引用或可递增的。

所以在我看来,输出的分配必须按顺序进行。它们是否只是意味着unary_op 的应用程序可能出现故障,并存储到临时位置,但按顺序复制到输出?这听起来不像是你想做的事情。

大多数 C++ 库实际上还没有实现并行执行器,但微软已经实现了。我很确定this 是相关代码,我认为它调用this populate() function 将迭代器记录到输出块中,这肯定不是一个有效的做法,因为@987654339 @ 可以通过增加它的副本来失效。

我错过了什么?

【问题讨论】:

  • godbolt 中的一个简单测试表明这是一个问题。使用 C++20 和transform 版本决定是否使用并行。大型向量的 transform 失败。
  • @Croolman 您的代码不正确,因为您正在回插入s,这会使迭代器无效。
  • @DanielsaysreinstateMonica 哦,炸肉排你是对的。正在对其进行调整并使其处于无效状态。我收回我的评论。
  • 有人闪回老问题是怎么回事?无论如何@DanielLangr 我的第一条评论代码是错误的,这就是你所指出的。当您在 godbolt 中将 back_inserter 更改为插入到 ordinals 时,它会编译并且似乎可以工作。
  • @alfC godbolt 代码有一个问题,即在std::back_inserter 中传入了s,而不是ordinals

标签: c++ stl language-lawyer c++17


【解决方案1】:

来自n4385

§25.6.4 变换

template<class InputIterator, class OutputIterator, class UnaryOperation>
constexpr OutputIterator
transform(InputIterator first1, InputIterator last1, OutputIterator result, UnaryOperation op);

template<class ExecutionPolicy, class ForwardIterator1, class ForwardIterator2, class UnaryOperation>
ForwardIterator2
transform(ExecutionPolicy&& exec, ForwardIterator1 first1, ForwardIterator1 last1, ForwardIterator2 result, UnaryOperation op);

template<class InputIterator1, class InputIterator2, class OutputIterator, class BinaryOperation>
constexpr OutputIterator
transform(InputIterator1 first1, InputIterator1 last1, InputIterator2 first2, OutputIterator result, BinaryOperation binary_op);

template<class ExecutionPolicy, class ForwardIterator1, class ForwardIterator2, class ForwardIterator, class BinaryOperation>
ForwardIterator
transform(ExecutionPolicy&& exec, ForwardIterator1 first1, ForwardIterator1 last1, ForwardIterator2 first2, ForwardIterator result, BinaryOperation binary_op);

§23.5.2.1.2 back_inserter

template<class Container>
constexpr back_insert_iterator<Container> back_inserter(Container& x);

返回:back_insert_iterator(x)。

§23.5.2.1 类模板 back_insert_iterator

using iterator_category = output_iterator_tag;

所以std::back_inserter 不能与std::transform 的并行版本一起使用。支持输出迭代器的版本使用输入迭代器从其源中读取。由于输入迭代器只能前后递增(第 23.3.5.2 节输入迭代器)并且只有顺序(ie 非并行)执行,因此必须在它们和输出迭代器之间保留顺序.

【讨论】:

  • 请注意,C++ 标准中的这些定义并没有避免提供特殊版本的算法,这些算法为其他类型的迭代器选择。例如,std::advance 只有一个定义采用 input-iterators,但 libstdc++ 为 bidirectional-iteratorsrandom-access-iterators 提供了附加版本我>。然后特定版本为executed based on the type of iterator passed
  • 我不认为你的评论是正确的 - ForwardIterators 并不意味着你必须按顺序做事。但是你已经强调了我错过的东西——他们使用ForwardIterator而不是OutputIterator的并行版本。
  • @Timmmm 我相信如果first1/last1参数是输入迭代器类型,或者result输出迭代器 类型,元素必须按顺序处理。因为除了使用++ 操作移动到下一次迭代之外,该实现没有任何其他选择。因为std::back_inserter_iterator输出迭代器,那么,这个条件成立。
  • 这个答案可能会受益于添加一些词来解释它的实际含义。
  • @Barry 添加了一些文字,非常感谢任何和所有反馈。
【解决方案2】:

所以我错过的是并行版本采用LegacyForwardIterators,而不是LegacyOutputIteratorLegacyForwardIterator 可以递增而不会使它的副本无效,因此很容易使用它来实现无序并行std::transform

我认为std::transform 的非并行版本必须按顺序执行。要么 cppreference 是错误的,要么标准可能只是隐含了这个要求,因为没有其他方法可以实现它。 (霰弹枪不是涉水通过标准找出来的!)

【讨论】:

  • 如果所有迭代器都足够强大,则非并行版本的变换可能会乱序执行。在问题的示例中,它们不是,因此 transform 的专业化 必须是有序的。
  • 不,他们可能不会,因为LegacyOutputIterator 会强制您按顺序使用它。
  • 它可以针对 std::back_insert_iterator&lt;std::vector&lt;T&gt;&gt;std::vector&lt;T&gt;::iterator 进行不同的专业化。第一个必须是有序的。第二个没有这个限制
  • 啊等一下,我明白你的意思了——如果你碰巧将一个LegacyForwardIterator 传递给非并行的transform,它可能有一个专门针对它的乱序。好点子。
【解决方案3】:

我相信转换可以保证按顺序处理。根据[back.insert.iterator]std::back_inserter_iterator 是一个输出迭代器(其iterator_category 成员类型是std::output_iterator_tag 的别名)。

因此,std::transform 对于如何进行下一次迭代别无选择,只能在 result 参数上调用成员 operator++

当然,这仅对没有执行策略的重载有效,其中std::back_inserter_iterator 可能无法使用(它不是转发迭代器)。


顺便说一句,我不会用 cppreference 中的引号来争论。那里的陈述通常不精确或简化。在这些情况下,最好查看 C++ 标准。其中,关于std::transform,没有关于操作顺序的引用。

【讨论】:

  • "C++ 标准。在哪里,关于 std::transform,没有关于操作顺序的引用" 既然没有提到顺序,那不是没有说明吗?
  • @HolyBlackCat 明确未指定,但由输出迭代器强加。请注意,对于输出迭代器,一旦增加它,就不能取消引用任何以前的迭代器值。
  • @DanielLangr,std::execution::par 呢?
  • @Sergei 不明白你的问题。如果它们可以并行发生,您将如何定义操作顺序?如果应该订购它们,则可能没有并行处理。
【解决方案4】:

1) 标准中的输出迭代器要求完全被打破。见LWG2035

2) 如果您使用纯输出迭代器和纯输入源范围,那么算法在实践中几乎无能为力;它别无选择,只能按顺序写。 (然而,一个假设的实现可以选择特殊情况下它自己的类型,比如std::back_insert_iterator&lt;std::vector&lt;size_t&gt;&gt;;我不明白为什么任何实现都想在这里这样做,但允许这样做。)

3) 标准中的任何内容都不能保证transform 按顺序应用转换。我们正在研究实现细节。

std::transform 只需要输出迭代器并不意味着它不能检测更高的迭代器强度并在这种情况下重新排序操作。实际上,算法始终根据迭代器强度调度,并且它们对特殊迭代器类型(如指针或向量迭代器)始终进行特殊处理。

当标准想要保证特定的顺序时,它知道怎么说(参见std::copy 的“从first 开始并继续到last”)。

【讨论】:

  • 我很惊讶 std::copy 不能作为 std::transform 的一个特例来实现。
  • 我不明白你关于专业化std::back_insert_iterator 的观点。你是说这种情况可以用operator+=来实现吗?使其有效地随机访问?因此允许std::transform 乱序执行操作。这会很疯狂,但我无法指出原因。我认为这是因为如果 ++* 未交错应用,则输出迭代器具有未定义的行为。仅此一项就应该阻止尝试实现+=(并且具有多次应用++ 的语义)。
  • 我认为“交错”要求拼写在以下短语中:“在此操作之后 (*) r 不需要可取消引用,并且不再需要 r 先前值的任何副本是可取消引用或可递增的。" 和 "在此操作之后 (++) r 不需要是可递增的,并且 r 的先前值的任何副本不再需要是可取消引用或可递增的。"。在这里:en.cppreference.com/w/cpp/named_req/OutputIterator。我认为这使得back_insert_iterator 的不同的乱序实现和专业化成为不可能。
猜你喜欢
  • 1970-01-01
  • 2011-09-14
  • 2020-08-03
  • 1970-01-01
  • 1970-01-01
  • 2014-12-06
  • 2021-01-03
  • 2010-10-28
  • 2016-04-22
相关资源
最近更新 更多