Search code examples
c++copy-constructorstdvectorlist-initialization

Using brace-initialization in member initializer list causes stack overflow in std::vector copy construction (with GCC but not with Clang)


A few days ago I watched Sean Parent's talk "Inheritance is the Base Class of Evil" and I decided to try his code out. While making a few changes I stumbled upon this weird behavior:

#include <vector>
#include <memory>
using namespace std;
class object_t {
  public:
    template <typename T>
    object_t(T x) : self_{make_unique<model<T>>(move(x))} {}
    //explicit object_t(T x) : self_{make_unique<model<T>>(move(x))} {} //this "solves" the problem
    object_t(const object_t &x) : self_(x.self_->copy_()) {}

  private:
    struct concept_t {
        virtual ~concept_t() = default;
        virtual unique_ptr<concept_t> copy_() const = 0;
    };
    template <typename T> struct model : concept_t {
        model(T x) : data_{move(x)} {} //*the behavior is caused by
                                       //this brace-initialization
        //model(T x) : data_(move(x)) {} //this works fine
        unique_ptr<concept_t> copy_() const override {
            return make_unique<model<T>>(*this);
        }
        T data_;
    };
    unique_ptr<const concept_t> self_;
};

int main() {
    object_t i{5};
    object_t v{vector<int>{1, 2, 3, 4}};
    object_t ic{i};
    object_t vc{v};

    vector<object_t> vv;
    vector<object_t> vvv1(vv);
    vector<object_t> vvv2 = vv;
    vector<object_t> vvv3{vv}; //this fails with a stack overflow in GCC 6.1.1 but only if brace-initialization is used in model<T>
}

I used GCC 6.1.1, Clang 3.8.0 and Clang 3.9.0 (a very recent build). The stack overflow only occurs when the code is compiled with GCC and only if brace initialization is used in model's constructor.

Here's the address sanitizer's output:

ASAN:DEADLYSIGNAL
=================================================================
==21125==ERROR: AddressSanitizer: stack-overflow on address 0x7fff8d492ff8 (pc 0x7f41c188eb02 bp 0x000000000020 sp 0x7fff8d493000 T0)
    #0 0x7f41c188eb01 in __sanitizer::StackDepotBase<__sanitizer::StackDepotNode, 1, 20>::Put(__sanitizer::StackTrace, bool*) /build/gcc-multilib/src/gcc/libsanitizer/sanitizer_common/sanitizer_stackdepotbase.h:96
    #1 0x7f41c188e657 in __sanitizer::StackDepotPut(__sanitizer::StackTrace) /build/gcc-multilib/src/gcc/libsanitizer/sanitizer_common/sanitizer_stackdepot.cc:110
    #2 0x7f41c17cffee in __asan::Allocator::Allocate(unsigned long, unsigned long, __sanitizer::BufferedStackTrace*, __asan::AllocType, bool) /build/gcc-multilib/src/gcc/libsanitizer/asan/asan_allocator.cc:420
    #3 0x7f41c17cffee in __asan::asan_memalign(unsigned long, unsigned long, __sanitizer::BufferedStackTrace*, __asan::AllocType) /build/gcc-multilib/src/gcc/libsanitizer/asan/asan_allocator.cc:703
    #4 0x7f41c1871df7 in operator new(unsigned long) /build/gcc-multilib/src/gcc/libsanitizer/asan/asan_new_delete.cc:60
    #5 0x406018 in std::_MakeUniq<object_t::model<std::vector<object_t, std::allocator<object_t> > > >::__single_object std::make_unique<object_t::model<std::vector<object_t, std::allocator<object_t> > >, std::vector<object_t, std::allocator<object_t> > >(std::vector<object_t, std::allocator<object_t> >&&) /usr/include/c++/6.1.1/bits/unique_ptr.h:787
    #6 0x40356b in object_t::object_t<std::vector<object_t, std::allocator<object_t> > >(std::vector<object_t, std::allocator<object_t> >) /home/theo/test/valuepoly.cpp:16
    #7 0x409169 in object_t::model<std::vector<object_t, std::allocator<object_t> > >::model(std::vector<object_t, std::allocator<object_t> >) /home/theo/test/valuepoly.cpp:25

//The same calls repeat again and again...

    #251 0x406046 in std::_MakeUniq<object_t::model<std::vector<object_t, std::allocator<object_t> > > >::__single_object std::make_unique<object_t::model<std::vector<object_t, std::allocator<object_t> > >, std::vector<object_t, std::allocator<object_t> > >(std::vector<object_t, std::allocator<object_t> >&&) /usr/include/c++/6.1.1/bits/unique_ptr.h:787
    #252 0x40356b in object_t::object_t<std::vector<object_t, std::allocator<object_t> > >(std::vector<object_t, std::allocator<object_t> >) /home/theo/test/valuepoly.cpp:16

SUMMARY: AddressSanitizer: stack-overflow /build/gcc-multilib/src/gcc/libsanitizer/sanitizer_common/sanitizer_stackdepotbase.h:96 in __sanitizer::StackDepotBase<__sanitizer::StackDepotNode, 1, 20>::Put(__sanitizer::StackTrace, bool*)
==21125==ABORTING

If I annotate object_t's constructor as explicit the problem goes away. I'm guessing that using the brace-initializer causes gcc to implicitly convert everything to object_t and use vector's std::initializer_list constructor; and this happens recursively.

My question is which of the two compilers behaves correctly?


Solution

  • This is the same as List-initialization priority from object of same type; clang is correct.

    [dcl.init.list]/3:

    [...] If T is a class type and the initializer list has a single element of type cv U, where U is T or a class derived from T, the object is initialized from that element [...]