【问题标题】:Is storing initializer_lists undefined behaviour? [duplicate]是否存储 initializer_lists 未定义的行为? [复制]
【发布时间】:2022-07-19 21:58:56
【问题描述】:

这个问题是对How come std::initializer_list is allowed to not specify size AND be stack allocated at the same time? 的跟进

简短的回答是,使用大括号括起来的列表 foo({2, 3, 4, 5, 6}); 调用函数在概念上在调用之前在堆栈空间中创建一个临时数组,然后传递初始化列表(如 string_view)仅引用此本地临时数组(可能在寄存器中):

int __tmp_arr[5] {2, 3, 4, 5, 6};
foo(std::initializer_list{arr, arr + 5});

现在考虑以下情况,其中我嵌套了对象 \"ref\" 的 initializer_lists。此 ref 对象在变体中递归地存储基元类型或 initializer_list。我现在的问题是:这是未定义的行为吗?它似乎适用于我的代码,但它符合标准吗?我怀疑的原因是,当内部构造函数调用嵌套的大括号括起来的列表返回时,初始化列表所引用的临时数组可能会因为堆栈指针被重置而失效(因此将 initializer_list 保存在变体中会保留一个无效的目的)。然后写入后续内存将覆盖初始化列表引用的值。我相信这个有错吗?

CompilerExplorer

#include <variant>
#include <string_view>
#include <type_traits>
#include <cstdio>

using val = std::variant<std::monostate, int, bool, std::string_view, std::initializer_list<struct ref>>;

struct ref
{
    ref(bool);
    ref(int);
    ref(const char*);
    ref(std::initializer_list<ref>);

    val value_;
};

struct container
{
    container(std::initializer_list<ref> init) {
        printf(\"---------------------\\n\");
        print_list(init);
    }

    void print_list(std::initializer_list<ref> list)
    {
        for (const ref& r : list) {
            if (std::holds_alternative<std::monostate>(r.value_)) {
                printf(\"int\\n\");
            } else if (std::holds_alternative<int>(r.value_)) {
                printf(\"int\\n\");
            } else if (std::holds_alternative<bool>(r.value_)) {
                printf(\"bool\\n\");
            } else if (std::holds_alternative<std::string_view>(r.value_)) {
                printf(\"string_view\\n\");
            } else if (std::holds_alternative<std::initializer_list<ref>>(r.value_)) {
                printf(\"initializer_list:\\n\");
                print_list(std::get<std::initializer_list<ref>>(r.value_));
            }
        }
    }
};

ref::ref(int init) : value_{init} { printf(\"%d stored\\n\", init); }
ref::ref(bool init) : value_{init} { printf(\"%s stored\\n\", init ? \"true\" : \"false\"); }
ref::ref(const char* init) : value_{std::string_view{init}} { printf(\"%s stored\\n\", init); }
ref::ref(std::initializer_list<ref> init) : value_{init} { printf(\"initializer_list stored\\n\", init); }

int main()
{
    container some_container = { 1, true, 5, { {\"itemA\", 2}, {\"itemB\", true}}};
}

输出:

1 stored
true stored
5 stored
itemA stored
2 stored
initializer_list stored
itemB stored
true stored
initializer_list stored
initializer_list stored
---------------------
int
bool
int
initializer_list:
initializer_list:
string_view
int
initializer_list:
string_view
bool
  • 您应该删除不相关的代码。您问的内容与std::variant 无关。
  • 让我们这样说吧:我只会在std::initializer_list 在范围内时使用它,就像任何其他局部变量一样。
  • 存储该列表可能不是 UB,但在“源”超出范围后访问其成员几乎可以肯定是。
  • @AdrianMole 那是我的问题。执行容器构造函数时源超出范围?
  • 临时生命周期以完整表达结束。

标签: c++ c++17 undefined-behavior initializer-list variant


【解决方案1】:

首先,复制std::initializer_list 不会复制底层对象。 (cppreference)


container some_container = { 1, true, 5, { {"itemA", 2}, {"itemB", true}}};

实际上编译成类似的东西

container some_container = { // initializer_list<ref>
   ref{1}, 
   ref{true}, 
   rer{5},
   ref{ // initializer_list<ref>
      ref{ // initializer_list<ref>
         ref{"itemA"}, 
         ref{2}
      }, 
      ref{ // initializer_list<ref>
         ref{"itemB"},
         ref{true}
      }
   }
};

并且所有这些对象的生命周期*在完整表达式结束时结束(; 这里)

*包括所有initializer_list&lt;ref&gt;,它们的底层数组,以及所有属于ref的对象,但不包括构造函数中的对象(虽然可能会应用复制省略)


所以

  1. 是的,可以在构造函数中使用这些对象。
  2. 那些对象在; 之后消失了,所以你不应该再使用存储的对象(在initializer_list 中)

    godbolt example with noisy destructor and action after the construction

【讨论】:

  • 谢谢,但我仍然有一个问题:让我们按级别枚举采用 initializer_lists 的构造函数:1,2 和 3,而 3 是构造函数,它采用包含“itemA”和 2 的初始化列表。现在在调用构造函数 3 之前,一个临时数组“itemA”和 2 在堆栈上预先分配,然后将其 initializer_list 传递给构造函数 3,后者将其存储在其成员中。但这发生在构造函数 2 的范围内,该构造函数在打印最后一个“initializer_list stored”后终止。现在不是官方之前提到的临时数组超出范围因此无效吗?
  • @glades 不,在完整表达式 ; 之后,数组也超出了范围(如链接示例中所示。所有析构函数仅在 container 的构造函数运行后调用)。
  • 即所有std::initializer_list 及其底层数组和所有ref 仅在完整表达式结束后被破坏。
  • @glades 如果您在构造函数中引用 std::initializer_list 按值传递。它确实超出了范围。但是在传递给构造函数之前创建的原始std::initializer_list 和底层对象不是,并且副本(您存储的)仍然可以毫无问题地访问它。
  • @glades 顺便说一句,重新“itemS”和 2 的临时数组预先分配在堆栈上,不,您传递的是std::initializer_list&lt;ref&gt;,所以数组实际上是{ref{"itemA"},ref{1}}
猜你喜欢
  • 1970-01-01
  • 2014-07-30
  • 2011-04-19
  • 1970-01-01
  • 2013-04-16
  • 2018-09-25
  • 1970-01-01
  • 2016-09-15
  • 1970-01-01
相关资源
最近更新 更多