在基于范围的for循环中对支撑的初始化列表进行不安全的使用吗?

时间:2017-04-26 03:17:53

标签: c++ c++11 language-lawyer clang++ list-initialization

这与question I asked earlier today非常相似。但是,我在这个问题中引用的例子不正确;错误的是,我正在查看错误的源文件,而不是实际出现错误的源文件。无论如何,这是我的问题的一个例子:

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 *>的直接列表初始化,因此临时支撑列表的内容将其生命周期延长到循环的持续时间? / p>

编辑:经过一些工作后,我现在有了一个自包含的示例,说明了我在应用程序中看到的行为:

#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;
}

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

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的写入,因此尝试访问未初始化的指针。我想这可能指向编译器错误。

1 个答案:

答案 0 :(得分:1)

我将分为两部分进行回答:元回答和具体回答。

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

这是不安全的使用初始化初始化列表吗?

当然可以。通常,您不应该自己构造初始化程序列表。在语言和标准库之间的接缝上,这是一个丑陋的细节。除非您绝对必须使用它,否则您应该忽略它的存在,即使那样-这样做是因为其他一些代码使您无法陷入自己的困境,而您需要成为语言律师才能从中爬出来。 definition of 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++;
}

?那就是您的示例应该编写的方式。

现在,您可能会问:“但是用括号括起来的初始化程序列表的类型是什么?”

好吧,我会说:

  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++;
}

初始化列表定义为:

在原始初始化程序列表对象的生命周期结束后,不能保证基础数组存在。

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