避免未定义的行为:临时对象

Avoiding undefined behaviour: temporary objects

我写了一个 class 用于将其用作方便的视图,例如在基于范围的 for 中。总的来说,它只是一对带有边界检查的迭代器:

template<typename I> class Range {
private:
  I begin;
  I end;

public:
  Range(I const begin, I const end)
    : begin(begin), end(end)
  {}

  Range<I>& skip(int const amount) {
    begin = std::min(begin + amount, end);
    return *this;
  }
};

template<typename C> auto whole(C const& container) {
  using Iterator = decltype(std::begin(container));
  return Range<Iterator>(std::begin(container), std::end(container));
}

这是它的预期用途(导致 UB):

std::vector<int> const vector{1, 2, 3};
for (int const value : whole(vector).skip(1)) {
  std::cout << value << ' ';
}

删除 skip(1) 部分有帮助,与 Range::skip 的以下重构相同:

Range<I> skip(int const amount) const {
  I const new_begin = std::min(begin + amount, end);
  return Range<I>(new_begin, end);
}

似乎临时文件不能 return 对自身的引用。这是 cppreference 所说的:

All temporary objects are destroyed as the last step in evaluating the full-expression that (lexically) contains the point where they were created, and if multiple temporary objects were created, they are destroyed in the order opposite to the order of creation.

尽管我不确定情况是否如此,而且我现在不知道如何实际解释它。实际问题是什么,我怎样才能可靠地避免这种 UB?是相似的表达,例如auto string = std::string("abc").append("def")也不安全?

关于 std::stringthe append functionappend 函数 return 是对它附加到的字符串的 引用 。在字符串对象被析构后保留这个引用,如果你使用它会导致未定义的行为。

但是 如果你复制字符串对象,那么你是安全的,因为你将有一个字符串的 copy 而不是对不存在对象的引用:

std::string s = std::string("abc").append("def");

此处 s 将是一个 copy,通过复制初始化进行初始化(它将 std::string("abc").append("def") 传递给 s 的复制构造函数,并且临时对象将贯穿整个构造函数)。


至于

for (int const value : whole(vector).skip(1)) { ... }

如果 Range<T> class 被修改为可迭代的(您需要 beginend 函数 到 return迭代器),那么它仍然不是UB。

那是因为这样的range-for循环对应的是这样的循环

for (auto iter = whole(vector).skip(1).begin();
     iter != whole(vector).skip(1).end();
     ++iter)
{
    ...
}

Range<T> class 不包含向量的副本,它包含向量的 迭代器 的副本(对于您显示的示例).这些迭代器将在 临时 Range<T> 对象被破坏之前被复制或使用。

ranged-based for loop 中,范围表达式 被绑定为引用(原型代码)

auto && __range = whole(vector).skip(1) ;

问题是由 whole(vector) 创建的临时对象在完整表达式引用 __range 之后立即销毁(它绑定到从 skip 返回的引用,即临时对象)变得悬垂;之后对它的任何取消引用都会导致 UB。

auto string = std::string("abc").append("def") 很好,string 被复制了,它是一个独立于临时 std::string.

的对象

自 C++20 起您可以添加 init-statement:

If range_expression returns a temporary, its lifetime is extended until the end of the loop, as indicated by binding to the forwarding reference __range, but beware that the lifetime of any temporary within range_expression is not extended.

例如

std::vector<int> const vector{1, 2, 3};
for (auto thing = whole(vector); int const value : thing.skip(1)) {
  std::cout << value << ' ';
}

range-for 包含对该范围的引用。你的例子

for (int const value : whole(vector).skip(1)) {
  std::cout << value << ' ';
}

被定义为等同于

{
    auto && __range = whole(vector).skip(1);
    auto __begin = __range.begin();
    auto __end = __range.end();
    for ( ; __begin != __end; ++__begin) {
        int const value = *__begin;
        std::cout << value << ' ';
    }
}

skip 返回的引用在第 ; 处无效。

你在哪里初始化一个对象,比如

std::string s = std::string("abc").append("def");

它是安全的,因为临时文件只需要比构造函数长。

顺便说一句,我更喜欢 Range<I> skip(int) const 而不是变异的。