lambda 返回 initializer_list 中的奇怪值

Posted

技术标签:

【中文标题】lambda 返回 initializer_list 中的奇怪值【英文标题】:Strange values in a lambda returning initializer_list 【发布时间】:2015-04-03 19:44:43 【问题描述】:

考虑一下这个 C++11 代码 sn-p:

#include <iostream>
#include <set>
#include <stdexcept>
#include <initializer_list>


int main(int argc, char ** argv)

    enum Switch 
        Switch_1,
        Switch_2,
        Switch_3,
        Switch_XXXX,
    ;

    int foo_1 = 1;
    int foo_2 = 2;
    int foo_3 = 3;
    int foo_4 = 4;
    int foo_5 = 5;
    int foo_6 = 6;
    int foo_7 = 7;

    auto get_foos = [=] (Switch ss) -> std::initializer_list<int> 
        switch (ss) 
            case Switch_1:
                return foo_1, foo_2, foo_3;
            case Switch_2:
                return foo_4, foo_5;
            case Switch_3:
                return foo_6, foo_7;
            default:
                throw std::logic_error("invalid switch");
        
    ;

    std::set<int> foos = get_foos(Switch_1);
    for (auto && foo : foos) 
        std::cout << foo << " ";
    
    std::cout << std::endl;
    return 0;

无论我尝试什么编译器,似乎都处理不正确。这让我觉得我做错了什么,而不是跨多个编译器的常见错误。

clang 3.5 输出:

-1078533848 -1078533752 134518134

gcc 4.8.2 输出:

-1078845996 -1078845984 3

gcc 4.8.3 输出(编译在http://www.tutorialspoint.com):

1 2 267998238

gcc(未知版本) 输出(编译于http://coliru.stacked-crooked.com)

-1785083736 0 6297428 

问题似乎是由于使用std::initializer_list&lt;int&gt; 作为 lambda 的返回值引起的。将 lambda 定义更改为 [=] (Switch ss) -&gt; std::set&lt;int&gt; ... 时,返回值正确。

请帮我解开这个谜。

【问题讨论】:

正如我在下面的回答中指出的那样,具有讽刺意味的是,initializer_list 的最终提案中指出了这种确切的情况并将其视为不太可能出现的问题。 【参考方案1】:

发件人:http://en.cppreference.com/w/cpp/utility/initializer_list

在原始初始化列表对象的生命周期结束后,不能保证底层数组存在。 std::initializer_list 的存储是未指定的(即它可以是自动、临时或静态只读内存,具体取决于具体情况)。

我不认为初始化列表是可复制构造的。 std::set 和其他容器都是。基本上,您的代码的行为类似于“返回对临时的引用”。

C++14 对底层存储的说法略有不同——延长 its 生命周期——但这并不能解决与 initializer_list 对象生命周期有关的任何问题,更不用说其副本。因此,即使在 C++14 中,问题仍然存在。

底层数组是一个临时数组,其中每个元素都是从原始初始化列表的相应元素复制初始化的(除了缩小转换无效)。底层数组的生命周期与任何其他临时对象相同,除了从数组初始化一个 initializer_list 对象会延长数组的生命周期,就像绑定一个临时对象的引用(有相同的例外,例如用于初始化非静态类成员)。底层数组可以分配在只读内存中。

【讨论】:

是的,就是这样。初始化列表由一个堆栈分配的数组支持,当 lambda 返回时,该数组会变得很糟糕。 initializer_list 是可复制的(因此可以编译),但它只执行浅拷贝。坦率地说,我发现这是一个糟糕的 C++11“特性”。幸运的是,是的,这在 C++14 中得到了修复,其中底层数组的生命周期在 initializer_list 的副本期间被延长,就像将它绑定到引用时一样。 不幸的是,GCC 4.9.2 在 C++14 模式 still gets it wrong。我没有用 HEAD 测试过。 确实如此。这不是一个非常有用的功能;-) “幸运的是,这种“疏忽”可以/应该在 C++14 中得到修复”,您粘贴的段落中的哪句话表明了这一点应该 i> 被修复并且这是一个疏忽?:“底层数组的生命周期是与任何其他临时对象相同,除了初始化一个initializer_list数组中的对象延长了数组的生命周期,就像将引用绑定到一个临时对象"。创建由其他引用类型变量初始化的引用不会延长原始临时的生命周期,直到 last 引用存在。数组是临时的 @LightnessRacesinOrbit 数组的生命周期被延长,直到用于初始化的 initializer_list 对象的生命周期结束;但是 initializer_list 对象是 lambda 的临时返回值,它的生命周期在 ; 处结束。 (这还不包括问题中的数组在 return 语句中被“绑定”的事实,所以通常你根本不会得到任何生命周期延长。)【参考方案2】:

问题是您正在引用一个不再存在的对象,因此您正在调用undefined behavior。 initializer_list 似乎在 C++11 draft standard 中未指定,没有规范部分实际指定此行为。尽管有很多注释表明这是行不通的,而且总的来说,尽管注释如果不与规范性文本冲突,则不是规范性的,但它们具有很强的指示性。

如果我们转到 18.9 部分 Initializer lists 它有一个注释说:

复制初始化列表不会复制底层元素。

8.5.4 部分我们有以下示例:

typedef std::complex<double> cmplx;
std::vector<cmplx> v1 =  1, 2, 3 ;

void f() 
    std::vector<cmplx> v2 1, 2, 3 ;
    std::initializer_list<int> i3 =  1, 2, 3 ;

带有以下注释:

对于 v1 和 v2,为 1, 2, 3 创建的 initializer_list 对象和数组具有完整表达式 寿命。对于 i3,initializer_list 对象和数组具有自动生命周期。

这些注释与给出以下示例的initializer_list proposal: N2215 一致:

std::vector<double> v = 1, 2, 3.14;

然后说:

现在将vector(initializer_list&lt;E&gt;) 添加到vector&lt;E&gt;,如上所示。现在, 该示例有效。初始化列表 1, 2, 3.14 被解释为 像这样构造的临时:

const double temp[] = double(1), double(2), 3.14  ;
initializer_list<double> tmp(temp,
sizeof(temp)/sizeof(double));
vector<double> v(tmp);

[...]

请注意,initializer_list 是一个小对象(可能是两个词), 所以按值传递它是有道理的。按值传递也简化了 begin() 和 end() 的内联以及对的常量表达式求值 大小()。

一个 initializer_list 将由编译器创建,但可以 被用户复制。把它想象成一对指针。

在这种情况下,initializer_list 只是保存指向自动变量的指针,该变量在退出范围后将不存在。

更新

我刚刚意识到提案实际上指出了这种误用场景

其中一个含义是 initializer_list 是“类似指针”的 就底层数组而言,它的行为就像一个指针。为了 示例:

int * f(int a)
 
   int* p = &a;
   return p; //bug waiting to happen


initializer_list<int> g(int a, int b, int c)

   initializer_list<int> v =  a, b, c ;
   return v; // bug waiting to happen
 

实际上需要一点聪明才智来滥用 initializer_list 这样。特别是,类型变量 initializer_list 将很少见

我觉得最后一句话(强调我的)特别讽刺。

更新 2

所以defect report 1290 修正了规范措辞,因此它现在涵盖了这种行为,尽管复制案例可能更明确。它说:

当初始化器列表时,出现了一个关于预期行为的问题 是类的非静态数据成员。初始化一个 initializer_list 是根据从一个 隐式分配的数组,其生命周期“与 initializer_list 对象”。这意味着该数组需要存在 只要 initializer_list 确实如此,从表面上看 似乎需要将数组存储在类似 同一类中的 std::unique_ptr (如果成员是 以这种方式初始化)。

如果这是本意,那将是令人惊讶的,但它会让 在此上下文中可用的 initializer_list。

决议修正了措辞,我们可以在N3485 version of the draft standard 中找到新措辞。所以8.5.4 [dcl.init.list] 部分现在说:

该数组与任何其他临时对象 (12.2) 具有相同的生命周期, 除了从数组中初始化一个 initializer_- 列表对象 延长数组的生命周期,就像绑定一个引用一样 临时的。

12.2 [class.temporary] 说:

临时绑定到函数中返回值的生命周期 return 语句 (6.6.3) 未扩展;临时被毁 在 return 语句中的完整表达式的末尾。

【讨论】:

@dyp 我看到你留下了一条评论,你已经删除了。如果您看到一个规范性部分,该部分像注释一样指定了生命周期和复制,请告诉我。 我认为临时数组与引用的绑定确实指定了生命周期(在 [dcl.init.list]/6 中)。这也符合一个奇怪的事实,即您可能在本地没有constexpr auto x = 1,2;,但constexpr static auto x = 1,2;:第一个示例中临时数组的生命周期延长到自动对象的生命周期,第二个示例延长到静态对象的生命周期.作为静态存储时长的对象,处理地址是合法的。 但这不是很明确,结果相当令人惊讶恕我直言。我猜想像template&lt;class T&gt; using id = T; auto&amp;&amp; il = id&lt;int[]&gt;1, 2; 这样明确地编写它可能是一个更好的主意。该数组是不可复制的,因此当您尝试将其传递给函数或尝试从函数返回时,您会看到奇怪的引用语义。 据我了解,lifetime和this example差不多,唯一的区别是写initializer_list&lt;int&gt; x = initializer_list&lt;int&gt;1,2,3;的时候也延长了lifetime(其实更像id&lt;int[]&gt;上面的例子,但是引用隐藏在intializer_list) @dyp yes 段落确实说生命周期与数组相同,但不包括 18.9 中的非规范注释所涵盖的复制。所以我认为这不足以证明它不起作用,或者至少它对我来说不够具体。考虑到我在提案中强调的最后一行,这似乎是一个疏忽。提议者认为这很明显,但显然不是。【参考方案3】:

因此,initializer_lists 在自身被复制或移动到复制/移动的结果时不会延长其引用数组的生命周期。这使得退货成为问题。 (它们确实将引用数组的生命周期延长到它们自己的生命周期,但这种扩展不能传递到省略或列表副本)。

要解决此问题,请手动存储数据并管理其生命周期:

template<size_t size, class T>
std::array<T, size> partial_array( T const* begin, T const* end ) 
  std::array<T, size> retval;
  size_t delta = (std::min)( size, end-begin );
  end = begin+delta;
  std::copy( begin, end, retval.begin() );
  return retval;

template<class T, size_t max_size>
struct capped_array 
  std::array<T, max_size> storage;
  size_t used = 0;
  template<size_t osize, class=std::enable_if_t< (size<=max_size) >>
  capped_array( std::array<T, osize> const& rhs ):
    capped_array( rhs.data(), rhs.data()+osize )
  
  template<size_t osize, class=std::enable_if_t< (size<=max_size) >>
  capped_array( capped_array<T, osize> const& rhs ):
    capped_array( rhs.data(), rhs.data()+rhs.used )
  
  capped_array(capped_array const& o)=default;
  capped_array(capped_array & o)=default;
  capped_array(capped_array && o)=default;
  capped_array(capped_array const&& o)=default;
  capped_array& operator=(capped_array const& o)=default;
  capped_array& operator=(capped_array & o)=default;
  capped_array& operator=(capped_array && o)=default;
  capped_array& operator=(capped_array const&& o)=default;

  // finish-start MUST be less than max_size, or we will truncate
  capped_array( T const* start, T const* finish ):
    storage( partial_array(start, finish) ),
    used((std::min)(finish-start, size))
  
  T* begin()  return storage.data(); 
  T* end()  return storage.data()+used; 
  T const* begin() const  return storage.data(); 
  T const* end() const  return storage.data()+used; 
  size_t size() const  return used; 
  bool empty() const  return !used; 
  T& front()  return *begin(); 
  T const& front() const  return *begin(); 
  T& back()  return *std::prev(end()); 
  T const& back() const  return *std::prev(end()); 

  capped_array( std::initializer_list<T> il ):
    capped_array(il.begin(), il.end() )
  
;

这里的目标很简单。创建一个基于堆栈的数据类型,存储一堆Ts,最多可以处理一个上限,并且可以处理更少。

现在我们将您的 std::initializer_list 替换为:

auto get_foos = [=] (Switch ss) -> capped_array<int,3> 
    switch (ss) 
        case Switch_1:
            return foo_1, foo_2, foo_3;
        case Switch_2:
            return foo_4, foo_5;
        case Switch_3:
            return foo_6, foo_7;
        default:
            throw std::logic_error("invalid switch");
    
;

并且您的代码有效。未使用空闲存储(无堆分配)。

更高级的版本将使用一组未初始化的数据并手动构造每个T

【讨论】:

你看,这件事可以用std::vector/std::set/std::list代替capped_array来完成。 std::initializer_list 的有用属性是可用于初始化它们中的每一个(std::vector/std::set/std::list)。只需std::&lt;something&gt; foo = get_foos(Switch_1);。这只是一个方便的问题,我想在我的代码中拥有漂亮。 @GreenScape 我想你在哪里试图避免空闲存储(堆上不必要的内存分配)。创建一个可用于构造几乎任意容器的类型很容易——只需重载template&lt;class C&gt;operator C() 并进行额外的SFINAE 测试,它可以通过(iterator, iterator) 构造。这就是为什么在您的问题中发布动机(如果只是作为旁白)是有用的。 你看,template&lt;class C&gt;operator C() 只启用简单的复制初始化。例如,如果我有一个std::set&lt;int&gt; a = ...;,然后我想向这个容器插入更多的值,使用std::initializer_list,这可以以非常干净的方式完成:a.insert(get_foos(Switch_1))。但是如果get_foos() 的返回值不是初始化列表,事情就会变得非常混乱。您必须在插入之前调用get_foos() 并将结果存储在某种辅助变量中,当您必须连续多次调用get_foos() 时,这不是很可读。 @GreenScape 然后用适当的重载实现C +concat= Xconcat( C, X )。在左侧,我们检测我们是序列容器还是关联容器(如果您真的想要,序列获取insert( end(c), s, f ),关联获取insert( s, f )。或者为关联容器和序列容器定义不同的操作(更容易,因为@ 987654345@ 过载和检测,这变得非常混乱)。诚然,在这一点上它比上面简单的更难。但是initializer_list 只是不起作用,所以...... 是的,我只是想要一个简单的解决方案,这似乎是可能的,但可惜的是,对于 C++ 来说不太可能,它会产生 UB :( 所以剩下的就是使用不太方便但简单的解决方案。就我而言,它是std::set。谢谢!

以上是关于lambda 返回 initializer_list 中的奇怪值的主要内容,如果未能解决你的问题,请参考以下文章

Java lambda 返回一个 lambda

我如何最终在 Lambda 函数中返回 promise 的值?

练习_使用Lambda表达式无参数无返回值的练习练习_使用Lambda表达式有参数有返回值的练习

确认后 ConfirmForgotPassword lambda 执行返回 InvalidLambdaResponseException - 无法识别的 lambda 输出

是否可以确定 lambda 的参数类型和返回类型?

AWS Lambda,API Gateway 返回 Malformed Lambda 代理响应,502 错误