Kemungkinan perilaku tidak terdefinisi dalam implementasi static_vector primitif

12

tl; dr: Saya pikir static_vector saya memiliki perilaku yang tidak jelas, tetapi saya tidak dapat menemukannya.

Masalah ini ada di Microsoft Visual C ++ 17. Saya memiliki implementasi static_vector yang sederhana dan belum selesai ini, yaitu vektor dengan kapasitas tetap yang dapat ditumpuk. Ini adalah program C ++ 17, menggunakan std :: aligned_storage dan std :: launder. Saya sudah mencoba merebusnya di bawah ini ke bagian-bagian yang menurut saya relevan dengan masalah ini:

template <typename T, size_t NCapacity>
class static_vector
{
public:
    typedef typename std::remove_cv<T>::type value_type;
    typedef size_t size_type;
    typedef T* pointer;
    typedef const T* const_pointer;
    typedef T& reference;
    typedef const T& const_reference;

    static_vector() noexcept
        : count()
    {
    }

    ~static_vector()
    {
        clear();
    }

    template <typename TIterator, typename = std::enable_if_t<
        is_iterator<TIterator>::value
    >>
    static_vector(TIterator in_begin, const TIterator in_end)
        : count()
    {
        for (; in_begin != in_end; ++in_begin)
        {
            push_back(*in_begin);
        }
    }

    static_vector(const static_vector& in_copy)
        : count(in_copy.count)
    {
        for (size_type i = 0; i < count; ++i)
        {
            new(std::addressof(storage[i])) value_type(in_copy[i]);
        }
    }

    static_vector& operator=(const static_vector& in_copy)
    {
        // destruct existing contents
        clear();

        count = in_copy.count;
        for (size_type i = 0; i < count; ++i)
        {
            new(std::addressof(storage[i])) value_type(in_copy[i]);
        }

        return *this;
    }

    static_vector(static_vector&& in_move)
        : count(in_move.count)
    {
        for (size_type i = 0; i < count; ++i)
        {
            new(std::addressof(storage[i])) value_type(move(in_move[i]));
        }
        in_move.clear();
    }

    static_vector& operator=(static_vector&& in_move)
    {
        // destruct existing contents
        clear();

        count = in_move.count;
        for (size_type i = 0; i < count; ++i)
        {
            new(std::addressof(storage[i])) value_type(move(in_move[i]));
        }

        in_move.clear();

        return *this;
    }

    constexpr pointer data() noexcept { return std::launder(reinterpret_cast<T*>(std::addressof(storage[0]))); }
    constexpr const_pointer data() const noexcept { return std::launder(reinterpret_cast<const T*>(std::addressof(storage[0]))); }
    constexpr size_type size() const noexcept { return count; }
    static constexpr size_type capacity() { return NCapacity; }
    constexpr bool empty() const noexcept { return count == 0; }

    constexpr reference operator[](size_type n) { return *std::launder(reinterpret_cast<T*>(std::addressof(storage[n]))); }
    constexpr const_reference operator[](size_type n) const { return *std::launder(reinterpret_cast<const T*>(std::addressof(storage[n]))); }

    void push_back(const value_type& in_value)
    {
        if (count >= capacity()) throw std::out_of_range("exceeded capacity of static_vector");
        new(std::addressof(storage[count])) value_type(in_value);
        count++;
    }

    void push_back(value_type&& in_moveValue)
    {
        if (count >= capacity()) throw std::out_of_range("exceeded capacity of static_vector");
        new(std::addressof(storage[count])) value_type(move(in_moveValue));
        count++;
    }

    template <typename... Arg>
    void emplace_back(Arg&&... in_args)
    {
        if (count >= capacity()) throw std::out_of_range("exceeded capacity of static_vector");
        new(std::addressof(storage[count])) value_type(forward<Arg>(in_args)...);
        count++;
    }

    void pop_back()
    {
        if (count == 0) throw std::out_of_range("popped empty static_vector");
        std::destroy_at(std::addressof((*this)[count - 1]));
        count--;
    }

    void resize(size_type in_newSize)
    {
        if (in_newSize > capacity()) throw std::out_of_range("exceeded capacity of static_vector");

        if (in_newSize < count)
        {
            for (size_type i = in_newSize; i < count; ++i)
            {
                std::destroy_at(std::addressof((*this)[i]));
            }
            count = in_newSize;
        }
        else if (in_newSize > count)
        {
            for (size_type i = count; i < in_newSize; ++i)
            {
                new(std::addressof(storage[i])) value_type();
            }
            count = in_newSize;
        }
    }

    void clear()
    {
        resize(0);
    }

private:
    typename std::aligned_storage<sizeof(T), alignof(T)>::type storage[NCapacity];
    size_type count;
};

Ini tampaknya berfungsi dengan baik untuk sementara waktu. Kemudian, pada satu titik, saya melakukan sesuatu yang sangat mirip dengan ini - kode sebenarnya lebih panjang, tetapi ini adalah intinya:

struct Foobar
{
    uint32_t Member1;
    uint16_t Member2;
    uint8_t Member3;
    uint8_t Member4;
}

void Bazbar(const std::vector<Foobar>& in_source)
{
    static_vector<Foobar, 8> valuesOnTheStack { in_source.begin(), in_source.end() };

    auto x = std::pair<static_vector<Foobar, 8>, uint64_t> { valuesOnTheStack, 0 };
}

Dengan kata lain, pertama-tama kita menyalin 8-byte Foobar struct ke dalam static_vector pada stack, kemudian kita membuat std :: pair dari static_vector dari 8-byte struct sebagai anggota pertama, dan uint64_t sebagai yang kedua. Saya dapat memverifikasi bahwa valuesOnTheStack berisi nilai yang tepat segera sebelum pasangan dibangun. Dan ... segfault ini dengan optimasi diaktifkan di dalam copy constructor static_vector (yang telah diuraikan ke dalam fungsi panggilan) ketika membangun pasangan.

Singkat cerita, saya memeriksa pembongkaran. Di sinilah segalanya menjadi agak aneh; asm yang dihasilkan di sekitar copy constructor inlined ditunjukkan di bawah ini - perhatikan bahwa ini berasal dari kode aktual, bukan sampel di atas, yang cukup dekat tetapi memiliki beberapa hal di atas konstruksi pasangan:

00621E45  mov         eax,dword ptr [ebp-20h]  
00621E48  xor         edx,edx  
00621E4A  mov         dword ptr [ebp-70h],eax  
00621E4D  test        eax,eax  
00621E4F  je          <this function>+29Ah (0621E6Ah)  
00621E51  mov         eax,dword ptr [ecx]  
00621E53  mov         dword ptr [ebp+edx*8-0B0h],eax  
00621E5A  mov         eax,dword ptr [ecx+4]  
00621E5D  mov         dword ptr [ebp+edx*8-0ACh],eax  
00621E64  inc         edx  
00621E65  cmp         edx,dword ptr [ebp-70h]  
00621E68  jb          <this function>+281h (0621E51h)  

Oke, jadi pertama-tama kita memiliki dua instruksi mov menyalin anggota hitungan dari sumber ke tujuan; sejauh ini bagus. edx memusatkan perhatian karena itu adalah variabel loop. Kami kemudian memiliki pemeriksaan cepat jika jumlahnya nol; itu bukan nol, jadi kami melanjutkan ke loop untuk di mana kami menyalin struct 8-byte menggunakan dua operasi mov 32-bit pertama dari memori untuk mendaftar, kemudian dari register ke memori. Tapi ada sesuatu yang mencurigakan - di mana kita mengharapkan perpindahan dari sesuatu seperti [ebp + edx * 8 +] untuk membaca dari objek sumber, hanya ada ... [ecx]. Kedengarannya tidak benar. Apa nilai ECX?

Ternyata, ecx hanya berisi alamat sampah, yang sama dengan yang kami segfaulting. Dari mana nilai ini didapat? Inilah ASM tepat di atas:

00621E1C  mov         eax,dword ptr [this]  
00621E22  push        ecx  
00621E23  push        0  
00621E25  lea         ecx,[<unrelated local variable on the stack, not the static_vector>]  
00621E2B  mov         eax,dword ptr [eax]  
00621E2D  push        ecx  
00621E2E  push        dword ptr [eax+4]  
00621E31  call        dword ptr [<external function>@16 (06AD6A0h)]  

Ini terlihat seperti panggilan fungsi cdecl lama yang biasa. Memang, fungsi memiliki panggilan ke fungsi C eksternal tepat di atas. Tetapi perhatikan apa yang terjadi: ecx digunakan sebagai register sementara untuk mendorong argumen pada stack, fungsinya dipanggil, dan ... kemudian ecx tidak pernah disentuh lagi sampai ia salah digunakan di bawah untuk membaca dari sumber static_vector.

Dalam praktiknya, isi ECX ditimpa oleh fungsi yang disebut di sini, yang tentu saja diperbolehkan untuk melakukannya. Tetapi bahkan jika tidak, tidak mungkin ecx akan berisi alamat untuk hal yang benar di sini - yang terbaik, itu akan menunjuk ke anggota stack lokal yang bukan static_vector. Sepertinya kompiler telah memancarkan beberapa perakitan palsu. Fungsi ini tidak pernah dapat menghasilkan output yang benar.

Jadi di situlah saya sekarang. Perakitan yang aneh ketika optimasi diaktifkan saat bermain-main di std :: mencuci tanah berbau bagiku seperti perilaku yang tidak terdefinisi. Tapi saya tidak bisa melihat dari mana itu berasal. Sebagai informasi tambahan tetapi sedikit berguna, dentang dengan bendera kanan menghasilkan perakitan yang sama dengan ini, kecuali dengan benar menggunakan ebp + edx, bukan ecx untuk membaca nilai.

pjohansson
sumber
Hanya pandangan sepintas, tetapi mengapa Anda memanggil clear()sumber daya yang Anda panggil std::move?
Batsyeba
Saya tidak melihat bagaimana itu relevan. Tentu, itu juga legal untuk meninggalkan static_vector dengan ukuran yang sama tetapi banyak objek yang dipindahkan. Konten akan dihancurkan ketika static_vector destructor tetap dijalankan. Tapi saya lebih suka meninggalkan vektor yang dipindahkan dengan ukuran nol.
pjohansson
Bersenandung. Di luar nilai gajiku. Punya suara positif karena ini adalah pertanyaan yang baik, dan mungkin menarik perhatian.
Batsyeba
Tidak dapat mereproduksi kerusakan apa pun dengan kode Anda (tidak terbantu karena tidak dikompilasi karena kekurangan is_iterator) berikan contoh minimal yang dapat direproduksi
Alan Birtles
1
btw, saya pikir banyak kode tidak relevan di sini. Maksud saya, Anda tidak memanggil operator penugasan di mana pun di sini sehingga dapat dihapus dari contoh
bartop

Jawaban:

6

Saya pikir Anda memiliki bug penyusun. Menambahkan __declspec( noinline )ke operator[]tampaknya untuk memperbaiki kecelakaan:

__declspec( noinline ) constexpr const_reference operator[]( size_type n ) const { return *std::launder( reinterpret_cast<const T*>( std::addressof( storage[ n ] ) ) ); }

Anda dapat mencoba melaporkan bug ke Microsoft tetapi bug tersebut sepertinya sudah diperbaiki di Visual Studio 2019.

Menghapus std::launderjuga tampaknya memperbaiki kerusakan:

constexpr const_reference operator[]( size_type n ) const { return *reinterpret_cast<const T*>( std::addressof( storage[ n ] ) ); }
Alan Birtles
sumber
Saya kehabisan penjelasan lain juga. Sebanyak itu menyebalkan mengingat situasi kita saat ini, tampaknya masuk akal bahwa inilah yang terjadi, jadi saya akan menandai ini sebagai jawaban yang diterima.
pjohansson
Menghapus pencucian memperbaikinya? Menghapus pencucian akan secara eksplisit menjadi perilaku yang tidak terdefinisi! Aneh.
pjohansson
@pjohansson std::launderdiketahui / dilaksanakan secara tidak benar oleh beberapa implementasi. Mungkin versi MSVS Anda didasarkan pada implementasi yang salah. Sayangnya, saya tidak memiliki sumbernya.
Fureeish