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

Jas*_*n R 9 c++ language-lawyer c++11 clang++ list-initialization

这与我今天早些时候提出问题非常相似。但是,我在该问题中引用的示例是不正确的。错误地,我查看的是错误的源文件,而不是实际出现我所描述的错误的文件。无论如何,这是我的问题的一个示例:

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
}
Run Code Online (Sandbox Code Playgroud)

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

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

请注意,唯一的变化是在括号括起的初始化程序列表周围有一组额外的括号。我仍在尝试将所有形式的初始化都付诸实践。我是否可以推测在第一个示例中,内部支撑初始化器是一个临时类,由于它是std::initializer_list复制构造函数的一个参数,因此不会在循环的整个生命周期内延长其寿命?基于前面关于基于范围的for的等效语句是什么的讨论,我认为是这种情况。

如果是这样,那么第二个示例是否成功,因为它使用的直接列表初始化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;
}
Run Code Online (Sandbox Code Playgroud)

如果我在具有Apple LLVM版本8.1.0(clang-802.0.42)的macOS上进行编译,如下所示:

clang++ -O3 -std=c++11 -o crash crash.cc
Run Code Online (Sandbox Code Playgroud)

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

我试图确定此示例是否由于上述临时生命周期问题而说明了未定义的行为(在这种情况下,我的代码将是错误的,并且将使用较新的clang版本),或者这是否可能只是一个这个特定的clang版本中的错误。

编辑2: 测试在这里进行。看起来在主线铛v3.8.1和v3.9.0之间发生了行为更改。在v3.9之前,该程序的有效行为是just int main() { return 0; },这是合理的,因为该代码没有副作用。但是,在更高版本中,编译器似乎优化了对它们的调用,operator new但将写操作保留t->x在循环中,因此尝试访问未初始化的指针。我想这可能指向编译器错误。

ein*_*ica 2

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

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

这种使用大括号初始化列表是不安全的吗?

当然如此。一般来说,您不应该自己构造初始值设定项列表。这是语言和标准库之间接缝处的一个丑陋的细节。你应该忽略它的存在,除非你绝对必须使用它,即使那样 - 这样做是因为其他一些代码让你,不要让自己陷入需要成为语言律师才能爬出来的困境。std::initializer_list 的定义有很多细枝末节,充满了狡猾的词语,如“代理”、“提供访问”(与“是”相对)、“可以实现为”(但也可能不实现)和“不保证在“之后存在”。啊。

更具体地说,您正在使用基于范围的 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++;
}
Run Code Online (Sandbox Code Playgroud)

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

现在,您可能会问:“但是用大括号括起来的初始值设定项列表的类型是什么?”

好吧,对此我想说:

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

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

你的循环,

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

相当于下面代码:

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++;
}
Run Code Online (Sandbox Code Playgroud)

初始化列表定义如下:

在原始初始化列表对象的生命周期结束后,不保证底层数组存在。

我不是一名语言律师,但我的猜测如下:似乎您正在使用内部初始化器_list 初始化外部初始化器_列表,因此无法获得内部初始化器_列表的保证生命周期扩展超出外部初始化器_列表的构造函数。因此,是否保留它取决于编译器的想法,并且不同编译器的不同版本在这方面的行为完全有可能不同。