空范围的 std::copy() 或 std::move() 是不是需要有效目的地?

Posted

技术标签:

【中文标题】空范围的 std::copy() 或 std::move() 是不是需要有效目的地?【英文标题】:should std::copy() or std::move() of empty range require valid destination?空范围的 std::copy() 或 std::move() 是否需要有效目的地? 【发布时间】:2013-10-20 17:37:44 【问题描述】:

下面代码中的std::move() 在Visual Studio 2013(带有调试配置)中编译时会发出运行时警告,因为它检测到destnullptr。但是,源范围是空的,因此永远不应访问 dest。 C++ 标准可能不清楚这是否应该被允许? 它声明:要求:结果不得在 [first,last) 范围内。 nullptr 似乎可以满足该要求。

#include <vector>
#include <algorithm>

int main() 
    std::vector<int> vec;
    int* dest = nullptr;
    // The range [begin(vec),end(vec)) is empty, so dest should never be accessed.
    // However, it results in an assertion warning in VS2013.
    std::move(std::begin(vec), std::end(vec), dest);

【问题讨论】:

@KerrekSB:不过,begin 并没有什么神奇之处。我可以创建自己的 beginend 方法,它们都返回 null。这怎么会违反什么? @Mehrdad:不会,这完全没有意义——你只能有空容器。这就是为什么它是一个明智的警告。我猜这就像一个死代码或未使用的警告。 @KerrekSB:这里不是警告,而是断言 error。但无论如何,我不明白为什么它没有意义——考虑std::array&lt;int, 0&gt;。我不知道标准是否实现了零大小的arrays,但如果实现了,我认为std::array::beginstd::array::end 返回null 是完全合理,不是吗? (零大小的数组本身也是合理的——它可以用于小字符串优化,如果他不想优化,用户可以指定0。) @Mehrdad:我现在不确定:它说begin() == end() 必须是“唯一值”——是“每个对象唯一”还是“每个数组类型唯一”?在后一种情况下,空指针就可以了,而断言确实太多了。 @KerrekSB:我不知道,但我怀疑该措辞被准确指定为允许使用空指针。 【参考方案1】:

不仅需要满足 Requires: 子句,还需要满足 Effects:Returns: 子句中的所有内容也是。让我们来看看它们:

效果:[first,last) 范围内的元素复制到[result,result + (last - first)) 范围内,从first 开始,并且 继续last

作为first == last,则[result, result + 0) 范围必须是有效范围。

[iterator.requirements.general]/p7 状态:

范围[i,i) 是一个空范围; ... Range [i,j) 有效当且仅当 j 可以从 i 访问。

和 p6 的同节说:

当且仅当迭代器 j 被称为可从迭代器 i 到达时 如果存在表达式++i 的有限应用序列 这使得i == j.

从这些段落中我得出结论:

int* dest = nullptr;

然后[dest, dest) 形成一个有效的空范围。所以 Effects: 段落中的第一句话在我看来还可以:

对于每个非负整数n &lt; (last - first),执行*(result + n) = *(first + n)

没有非负整数n &lt; 0,因此不能进行赋值。所以第二句不禁止dest == nullptr

返回: result + (last - first).

[expr.add]/p8 特别允许将 0 加到任何指针值上,结果比较等于原始指针值。因此dest + 0 是等于nullptr 的有效表达式。 Returns: 子句没有问题。

要求: result 不得在 [first,last) 范围内。

我认为没有合理的方法来解释 dest 将“在”一个空范围内。

复杂性:正是last - first 分配。

这确认不能完成任何任务。

我在标准中找不到任何声明使这个示例除了格式良好之外什么都没有。

【讨论】:

我希望你能过来。能给出权威回答的人似乎不多。 :) 该标准的编写方式对不执行任何操作的调用没有任何要求(std::move() 无容器元素);检查目的地,即使没有被访问,也是笨拙和低效的。然而,我想知道它是一个多么严重的编译器错误,无论是形式上还是考虑到证明没有元素被复制的难度。 奇异值迭代器([iterator.requirements.general]/p5)怎么样?单值迭代器不能与任何东西进行比较。如果nullptr 被认为是一个奇异值迭代器,那么[nullptr, nullptr) 在技术上不是一个空范围,因为技术上我们不能通过== 看到它是空的。 (我确实意识到它可以在任何理智的编译器上工作,因为 C 指针在这里允许的不仅仅是迭代器的抽象概念。)所以问题是:nullptr 是奇异值迭代器吗?为什么? @AdamBadura:[conv.ptr]/p1 和 [expr.eq]/p1 都提到nullptr 可以进行相等比较。 @HowardHinnant 这似乎无关紧要。例如std::copy 会将结束迭代器视为过去的迭代器并且不会取消引用它。即使实际值不是过去的迭代器。同样,仅因为nullptr 可比较作为指针,它并不一定意味着它可比较作为迭代器。因为作为一个迭代器,它可能被认为是一个奇异值迭代器。 如果确实如此,那么std::copy 会正确地将[nullptr, nullptr) 视为无效范围,就像它对从奇异值迭代器构建的任何范围一样。【参考方案2】:

Visual Studio 中的 STL 调试版本会进行额外的参数验证。在这种情况下,它正在验证 dest 不为空,因为它不应该是,这是失败的。发布版本可能会按照您的预期运行,从不使用 dest,但这不会使输入数据有效。

STL 的调试版本试图通过说“您的输入错误”来帮助您。虽然在某些情况下错误的输入可能不是问题,但验证器无法知道您在什么条件下传递了错误的数据。就个人而言,我宁愿让 VS 告诉我调试版本中的错误输入,也不愿在生产中抛出运行时异常。

当然,你可以这样做:

int* dest = nullptr;
if (vec.size() > 0) dest = realDest;
std::move(std::begin(vec), std::end(vec), dest);

但是验证器不知道,所以它假设最坏的情况,特别是因为修复它对您来说非常容易(只需始终传递一个有效的输出迭代器)并且不警告您它可能会对您的应用程序产生可怕的后果生产中的运行时。

【讨论】:

我认为您错过了问题的重点。我似乎 OP 更关心标准及其特定的措辞。在这种情况下,标准似乎有些未指定。 是的,你完全错过了问题的重点......我认为 OP 不需要解释为什么调试版本比发布版本更适合调试。 -1【参考方案3】:

这个答案来自@Philipp Lenk 的评论。如果他提供了答案并且您认为可以接受,请选择他的而不是我的,并请投票赞成他的原始评论。

§25.1.5:在本条款中,模板参数的名称是 用于表达类型要求 [...] 如果算法的模板 参数是 OutputIterator、OutputIterator1 或 OutputIterator2,则 实际模板参数应满足输出的要求 迭代器。

§24.2.4:类或指针类型 X 满足 如果 X 满足迭代器要求并且 表 108 中的表达式是有效的并且具有指定的语义。 表格的第一行:*r = o,备注post: r is incrementable.

int* dest = nullptr;
*dest = 5;
++dest;

以上代码无效。在这种情况下,您不能分配给 *dest,因此根据标准,您不能为 dest 传递 nullptr。

【讨论】:

§24.2.4 讨论的是对类型的要求,而不是参数 感谢您给予我信任并制定此答案,但我担心 Mehrdad 是对的,它正在谈论类型,而我最初认为它适用于这种情况的想法是错误的。据我现在了解,它仅适用于 nullptr 常量已直接传递给 std::move 的情况,因为在这种情况下,类型将为 nullptr_t 在这种情况下,示例程序仍然无效,因为dest 被用作输出迭代器。在示例中将nullptr 分配给dest 后,dest 就不再是有效的输出迭代器。为了使某些东西成为有效的迭代器,您必须能够对其执行*r = o 操作,而您不能对nullptr 执行操作。编译器允许您将nullptr 分配给dest,但一旦您这样做,dest 就不再是有效的输出迭代器,因此您不能将其传递给采用输出迭代器并期望得到有效结果的东西. 按照这个逻辑,std::end(vec) 也不是一个有效的输入迭代器,对吧?所以我认为这个逻辑是不对的。 如前所述,我以前也是这么想的,但是错了。由于“int dest=nullptr”这一行,可解引用性仅仅是迭代器type的要求,在这种情况下它是int*,而不是nullptr_t。而 int 类型确实满足所有要求,具体值与那些无关。 Howard Hinnants 的回答解释得很透彻。

以上是关于空范围的 std::copy() 或 std::move() 是不是需要有效目的地?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用ostream_iterator检查copy_if是不是为范围内的任何内容返回true?

需要彻底替换对 std::copy 的可能不安全的调用

使用 std::copy 时出错

为啥在 std::copy 期间使用 std::back_inserter 而不是 end()?

在结构数组上使用 c++ std::copy

使用 `std::copy()` 和 `std::back_inserter()`