【问题标题】:Is this unsafe usage of a braced initializer list in a range-based for loop?在基于范围的 for 循环中使用大括号初始化列表是不安全的吗?
【发布时间】:2017-09-23 06:40:48
【问题描述】:

这与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&lt;base *&gt; 的直接列表初始化,因此临时花括号列表的内容将其生命周期延长到循环的持续时间?

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

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

【问题讨论】:

  • 我看不出代码有任何问题。即使使用 std::initializer 列表不是一个好主意,它也没有什么问题。并且在范围基础 for 循环期间初始化列表中的数据的生命周期是有效的。因此,我看不出有任何理由将列表用作问题的根源。顺便说一句:如果你简单地用Base* ptr1 = new Child1生成你的指针......你可以直接写for( auto ptr: { ptr1, ptr2 })...因为所有的ptr都有相同的类型(基数)所以类型推导没有问题。如果使用基类型的智能指针也是一样。

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


【解决方案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&lt;Test *&gt; &amp;&amp;,但真的,忘记这个事实。)

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

你的循环,

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

初始化列表定义说:

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

我不是语言律师,但我的猜测如下:似乎您正在使用内部初始化器初始化外部初始化器列表,因此无法保证内部初始化器的生命周期延长超出外部构造函数初始化器列表。因此,是否保持其存活取决于编译器的心血来潮,并且完全有可能不同编译器的不同版本在这方面表现不同。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-10-21
    • 1970-01-01
    • 1970-01-01
    • 2014-04-07
    • 1970-01-01
    • 1970-01-01
    • 2016-10-31
    相关资源
    最近更新 更多