在基于范围的for循环中使用大括号的初始化程序列表是否不安全? [英] Is this unsafe usage of a braced initializer list in a range-based for loop?

查看:60
本文介绍了在基于范围的for循环中使用大括号的初始化程序列表是否不安全?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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复制构造函数的一个参数,因此不会在循环的整个生命周期内延长其寿命? 基于前面关于基于范围的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;
}

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

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

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

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

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

解决方案

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

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

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

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

与以下代码等效

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.因此,是否保留其生存权取决于编译器的想法,并且不同编译器的不同版本在这方面的行为完全可能不同.

This is very similar to a question I asked earlier today. However, the example I cited in that question was incorrect; by mistake, I was looking at the wrong source file, not the one that actually had the error I described. Anyway, here's an example of my problem:

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
}

This was the site of some subtle memory corruption bugs in my application. After experimenting a bit, I found that changing the above to the following made the crashes that I was observing go away:

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

Note that the only change was an extra set of parentheses around the braced initializer list. I'm still trying to wrap my mind around all of the forms of initialization; am I right in surmising that in the first example, the inner braced initializer is a temporary that doesn't get its lifetime extended for the lifetime of the loop, due to the fact that it is an argument to the std::initializer_list copy constructor? Based on this previous discussion of what the equivalent statement for the range-based for is, I think that's the case.

If this is true, then does the second example succeed because it uses direct list-initialization of the std::initializer_list<base *>, and therefore the contents of the temporary braced list get their lifetime extended to the duration of the loop?

Edit: After some more work, I now have a self-contained example that illustrates the behavior that I'm seeing in my application:

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

If I compile this on macOS with Apple LLVM version 8.1.0 (clang-802.0.42), as follows:

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

then the resulting program dies with a segfault when I run it. Using an earlier version of clang (8.0.0) doesn't exhibit the issue. Likewise, my tests with gcc on Linux did not have any issues either.

I'm trying to determine whether this example illustrates undefined behavior due to the temporary lifetime issues I mentioned above (in which case my code would be in the wrong, and the newer clang version would be vindicated), or whether this is potentially just a bug in this particular clang release.

Edit 2: The test is live here. It looks like the change in behavior occurred between mainline clang v3.8.1 and v3.9.0. Previous to v3.9, the effective behavior of the program was just int main() { return 0; }, which is reasonable as there are no side effects to the code. In later versions, however, the compiler seems to optimize out the calls to operator new but keeps the writes to t->x in the loop, hence trying to access an uninitialized pointer. I suppose that might point to a compiler bug.

解决方案

I'll answer in two parts: A meta-answer and specific one.

The general (and more relevant) answer: Don't use initializer_list's unless you have to.

Is this unsafe usage of a braced initializer list?

Of course it is. Generally, you shouldn't construct initializer lists yourself. It's an ugly piece of detail on the seam between the language and the standard library. You should ignore its very existence unless you absolutely have to work with it, and even then - do so because some other code made you, don't put yourself into holes from which you need to be a language lawyer to climb out. The definition of std::initializer_list has a lot of fine print full of weasel words like "proxy", "provides access to" (as opposed to "is"), "may be implemented as" (but also may not) and "is not guaranteed to exist after". Ugh.

More specifically, you're using a ranged-based for loop. Whatever made you want to explicitly instantiate an std::initializer_list? What's wrong with the following:

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

? That's the way your example should be written.

Now, you might be asking: "But what's the type of that brace-enclosed initializer list?"

Well, to that I would say:

  1. What's it to you? The intent is clear, and there is no reason to believe any segfault should occur here. If this code had not worked, you could (and should) have complained straight to the standard committee about how the language was brain-dead.
  2. I don't know. Or I rather, I make it a point not to know unless I really have to.
  3. (Ok, just between you and me - it's std::initializer_list<Test *> &&, but really, forget that fact.)

About your specific final version of the code

Your loop,

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

is equivalent to the following code:

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 initializer list definition says:

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

I am not a language lawyer, but my guess is as follows: It seems like you're initializing the outer initializer_list with an inner one, and thus not getting a guaranteed lifetime extension of the inner one beyond the constructor of the outer initializer_list. It is therefore up to the compiler's whim whether to keep it alive or not, and it's entirely possible that different versions of different compilers behave differently in this respect.

这篇关于在基于范围的for循环中使用大括号的初始化程序列表是否不安全?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆