不使用违规表达式时的未定义行为?

时间:2014-06-27 13:03:34

标签: c++ c++11 iterator language-lawyer undefined-behavior

comment @MarcvanLeeuwen another Q&A中,有人建议以下是未定义的行为(UB):

template<class FwdIt, class T>
FwdIt find_before(FwdIt before_first, FwdIt last, T const& value)
{
    return std::adjacent_find(before_first, last, 
        [&](auto const& /* L */, auto const& R) { 
        // note that the left argument of the lambda is not evaluated
        return R == value; 
    });
}

auto list = std::forward_list<int> { 1, 2 };
auto it = find_before(list.before_begin(), list.end(), 1); // undefined behavior!?

UB来自以下事实:提供给BinaryPredicate的{​​{1}}将一次取消引用两个相邻的迭代器,第一对是std::adjacent_findlist.before_begin()。由于list.begin()不可解除引用,这可能需要UB。另一方面,左边的参数永远不会在lambda中使用。有人可能会说,在&#34; as-if&#34;规则是否无法观察到derefence是否实际发生,因此优化编译器可能完全忽略它。

问题:当实际上没有使用表达式before_begin()并且可以对其进行优化时,标准对取消引用不可重复的迭代器it的说法是什么? (或者更常见的是,当不使用违规表达时是UB吗?)

注意:通过特殊套管第一个迭代器很容易解决有问题的代码(如更新的原始Q&amp; A中所做的那样)。

3 个答案:

答案 0 :(得分:1)

before_begin()产生一个不可解除引用的迭代器;应用一元*的行为具有未定义的行为,因此上述代码肯定有未定义的行为:

  

1.3.24 [defns.undefined]

     

未定义的行为
  本国际标准没有要求的行为

现在,对于库的某些实现来说,上述代码有可能具有已定义的行为;例如,在forward_list的简单实现中,其迭代器上的operator*可能会对未对齐存储的未初始化区域形成T&引用:

template<typename T> struct forward_list {
    struct Node {
        Node* next = nullptr;
        typename aligned_storage<sizeof(T), alignof(T)>::type buf;
    };
    struct iterator {
        Node* node;
        T& operator*() { return *static_cast<T*>(static_cast<void*>(node->buf)); }
    };
    Node head;
    iterator before_begin() { return {&head}; }
};

在这种情况下,*(list.before_begin())已经定义了行为,只要结果仅以3.8p6允许的“有限方式”使用。

但是,对此方案的任何小改动都可能导致*(list.before_begin())变得不确定;例如,优化空间实现可以使用省略存储的Node_base,并从具体列表节点派生它。或者对齐的存储可能包含一个包裹T的对象,因此访问包裹的T会导致3.8p6的犯规。或者,调试实现可以​​检查取消引用before-begin迭代器并终止程序。

通过as-if规则,允许实现消除没有可见副作用的评估,但这不会导致代码具有已定义的行为。如果实现调试 - 检查取消引用,程序终止肯定是副作用,因此不可消除。

更有趣的是,如果在形成T&引用的过程中发生了具有未定义行为的操作,那么优化编译器可能会使用它来推断导致未定义行为的代码路径不会发生。根据{{​​1}}的参考实现,消除导致违规adjacent_find的代码路径会导致*first 总是返回find_before的结果。< / p>

答案 1 :(得分:1)

  

23.3.4.3 forward_list迭代器[forwardlist.iter]

iterator before_begin() noexcept;
const_iterator before_begin() const noexcept;
const_iterator cbefore_begin() const noexcept;
     

1返回:一个不可解除引用的迭代器,当递增时,等于begin()返回的迭代器。
  3备注:before_begin()== end()应等于false。

考虑到句子3,如果像你一样调用adjacent_find,如果列表中至少有一个元素(elem1:before_begin(),elem2:begin(),end,则会取消引用第一个迭代器:end())。

句子1表示由before_begin返回的迭代器不是可解除引用的。不。在任何情况下。没有任何if或when。取消引用迭代器是未定义的行为,标准并不是说你可以取消引用迭代器并丢弃*before_begin()的返回值。请记住,adjacant_find需要取消引用迭代器,以便它可以将解除引用返回到谓词的任何内容传递。

与未定义的行为一样,编译器可以随心所欲地执行任何操作。如果编译器在优化阶段看到他可以内联你的谓词,并且如果认为左迭代器不需要被解除引用,因为你没有使用返回的值,他可能会生成刚才这样做的代码,因此不会崩溃。在未定义行为的情况下,生成与您期望的一样工作的代码是一种可能,但您不能指望它。

顺便说一句,你为什么要adjacent_find使用只看右侧的谓词?你不能使用find吗?

答案 2 :(得分:0)

假设这个论点,这里有明确的未定义行为 名字意味着他们的意思,但它是在呼叫者 码。在第一次之前没有迭代器这样的东西,并且 只是尝试创建一个是未定义的行为。该 未定义的行为并非来自试图解除引用 迭代器;它来自它的存在。 (当然, 在致电您之前,std::adjacent_find 取消引用它 lambda,所以如果迭代器存在,并且不能解引用, 这也是未定义的行为。)