Search code examples
c++c++11g++initializer-listclang++

Lifetime of references / intilializer_list


Consider following example:

First compilation unit:

#include <vector>
#include <string>
#include <initializer_list>
#include <iostream>

struct DoubleString
{
  std::string one;
  std::string two;
};

class E
{
  public:
  E(std::initializer_list<DoubleString> init) : stringVec(std::move(init))
  {}

  void operator()()
  {
    for (auto const & x : stringVec)
    {
      std::cout << x.one << " " << x.two << std::endl;
    }
  }

  private:
    std::initializer_list<DoubleString> stringVec;
};

class F
{
  public:
    F( const std::string & one, const std::string & two) : e{ {one, two} }
    { }

    void operator()()
    {
      e();
    }

  private:
    E e;
};

class Caller
{
  public:
    void operator[](F f);
};


int main()
{
  Caller()[ F{"This is string 1", "This is string 2"} ];
}

Separate Compilation Unit:

void Caller::operator[](F f)
{
  f();
}

See also http://coliru.stacked-crooked.com/a/b01d349fa8f22f62

Compiling and running this with gcc and clang, both snippets in one compilation unit, output is "This is string 1 This is string 2"

When I move void Caller::operator [](F f) into a separate compilation unit, it still works for gcc but breaks for clang (it prints garbage). The Clang address sanitizer detects:

==16368==ERROR: AddressSanitizer: stack-buffer-underflow on address 0x7ffc6602f388 at pc 0x0000006d036a bp 0x7ffc6602f340 sp 0x7ffc6602f338

When I use std::vector as type for variable E::stringVec, it works correctly for clang again.

It seems that I misuse the std::initializer_list. Is it allowed to use it as a variable? Why does it work for gcc but not for clang?

BTW: I like the Coliru as Online-Compiler. Does anyone know, how to define separate compilation units?


Solution

  • The original std::initializer_list<DoubleString> is created as part of the constructor call of F (when passing it to member e). When creating this std::initializer_list<DoubleString> the DoubleString object is created. The life of the std::initializer_list<DoubleString> and, thus, the DoubleString object ends when the e member is finished to initialize.

    However, std::initializer_list<T> isn't really a value type. It is copyable but the copy doesn't create a copy but merely copies the stack pointers to the objects used to create the std::initializer_list<T>. This, the copy in E actually points to a sequence of objects (well, just one) which are destroyed once the original st::initializer_list<DoubleString> goes away. That is, you have simply undefined behavior. Why things work in one setting and not the other isn't really clear but that's kind of in the nature of undefined behavior.

    The basic take-away is this: std::initializer_list<T> isn't really a first class citizen. It is, essentially, a hack to get hold of a sequence of stack-allocated objects without copying them to allow initialization of sequences. It really has no place outside argument lists which should be capable of consuming an unbounded number of arguments of the same type.