在基于范围的 for 循环中使用花括号初始化列表是不安全的吗?

Is this unsafe usage of a braced initializer list in a range-based for loop?

这与 非常相似。但是,我在该问题中引用的示例是不正确的;我错误地查看了错误的源文件,而不是我描述的实际有错误的文件。无论如何,这是我的问题的一个例子:

struct base { };
struct child1 : base { };
struct child2 : base { };

child1 *c1;
child2 *c2;

// Goal: iterate over a few derived class pointers using a range-based for loop.
// initializer_lists are convenient for this, but we can't just use { c1, c2 }
// here because the compiler can't deduce what the type of the initializer_list
// should be. Therefore, we have to explicitly spell out that we want to iterate
// over pointers to the base class.
for (auto ptr : std::initializer_list<base *>({ c1, c2 }))
{
   // do something
}

我的应用程序中存在一些微妙的内存损坏错误。经过一些试验,我发现将上面的内容更改为以下内容可以使我观察到的崩溃消失:

for (auto ptr : std::initializer_list<base *>{ c1, c2 })
{
   // do something
}

请注意,唯一的变化是花括号初始化列表周围多了一组括号。我仍在努力思考所有的初始化形式;我是否正确地推测在第一个示例中,内部大括号初始化程序是一个临时的,它的生命周期不会在循环的生命周期内延长,因为它是 std::initializer_list 副本的参数构造函数? Based on this previous discussion of what the equivalent statement for the range-based for is,我觉得是这样的

如果这是真的,那么第二个例子是否成功,因为它使用 std::initializer_list<base *> 的直接列表初始化,因此临时花括号列表的内容将它们的生命周期延长到循环的持续时间?

编辑: 经过一些更多的工作,我现在有一个独立的示例来说明我在我的应用程序中看到的行为:

#include <initializer_list>
#include <memory>

struct Test
{
    int x;
};

int main()
{
    std::unique_ptr<Test> a(new Test);
    std::unique_ptr<Test> b(new Test);
    std::unique_ptr<Test> c(new Test);
    int id = 0;
    for(auto t : std::initializer_list<Test*>({a.get(), b.get(), c.get()}))
        t->x = id++;
    return 0;
}

如果我在 macOS 上使用 Apple LLVM 版本 8.1.0 (clang-802.0.42) 编译它,如下所示:

clang++ -O3 -std=c++11 -o crash crash.cc

然后生成的程序在我 运行 时因段错误而终止。使用较早版本的 clang (8.0.0) 不会出现此问题。同样,我在 Linux 上使用 gcc 进行的测试也没有任何问题。

我正在尝试确定此示例是否说明了由于我上面提到的临时生命周期问题导致的未定义行为(在这种情况下我的代码会出错,并且较新的 clang 版本会被证明是正确的),或者是否这可能只是这个特定 clang 版本中的一个错误。

编辑 2:The test is live here。看起来行为的变化发生在主线 clang v3.8.1 和 v3.9.0 之间。在 v3.9 之前,程序的有效行为只是 int main() { return 0; },这是合理的,因为代码没有副作用。然而,在更高版本中,编译器似乎优化了对 operator new 的调用,但在循环中保留对 t->x 的写入,因此试图访问未初始化的指针。我想这可能指向一个编译器错误。

我将分两部分回答:元答案和具体答案。

一般(和更相关的)答案:除非必须,否则不要使用 initializer_list

Is this unsafe usage of a braced initializer list?

当然是。通常,您不应该自己构造初始化列表。这是语言和标准库之间接缝处的一个丑陋的细节。你应该忽略它的存在,除非你绝对必须使用它,即使那样 - 这样做是因为其他一些代码让你,不要让自己陷入需要成为语言律师才能爬出来的洞中。 definition of std::initializer_list 有很多满是狡猾字眼的精美印刷品,例如 "proxy"、"provides access to"(相对于 "is")、"may be implemented as"(但也可能不是)和 "is not guaranteed to exist after"。啊。

更具体地说,您正在使用基于范围的 for 循环。是什么让您想要显式实例化一个 std::initializer_list?以下内容有什么问题:

struct Test { int x; };

int main() {
    std::unique_ptr<Test> a(new Test);
    std::unique_ptr<Test> b(new Test);
    std::unique_ptr<Test> c(new Test);
    int id = 0;
    for( auto t : {a.get(), b.get(), c.get()} )
         t->x = id++;
}

?这就是你的例子应该写的方式。

现在,您可能会问:"But what's the type of that brace-enclosed initializer list?"

嗯,对此我要说:

  1. 这对你有什么好处?意图很明确,没有理由相信这里会发生任何段错误。如果此代码不起作用,您可以(并且应该)直接向标准委员会抱怨该语言是如何脑死亡的。
  2. 我不知道。或者我更确切地说,除非我真的必须知道,否则我不会知道。
  3. (好吧,就你我而言 - 它是 std::initializer_list<Test *> &&,但真的,忘记这个事实吧。)

关于你代码的具体最终版本

你的循环,

for(auto t : std::initializer_list<Test*>({a.get(), b.get(), c.get()})) { t->x = id++; }

equivalent以下代码:

auto && range = std::initializer_list<Test*>({a.get(), b.get(), c.get()});
for (auto begin = std::begin(range), end = range.end(); begin != end; ++begin) {
    auto t = *begin;
    t->x = id++;
}

初始化列表定义说:

The underlying array is not guaranteed to exist after the lifetime of the original initializer list object has ended.

我不是语言律师,但我的猜测如下:看起来你正在用内部初始化外部 initializer_list,因此不能保证内部的生命周期延长超出外initializer_list的构造函数。因此,是否保留它取决于编译器的心血来潮,并且完全有可能不同版本的不同编译器在这方面表现不同。