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.
sumber
clear()
sumber daya yang Anda panggilstd::move
?is_iterator
) berikan contoh minimal yang dapat direproduksiJawaban:
Saya pikir Anda memiliki bug penyusun. Menambahkan
__declspec( noinline )
keoperator[]
tampaknya untuk memperbaiki kecelakaan:Anda dapat mencoba melaporkan bug ke Microsoft tetapi bug tersebut sepertinya sudah diperbaiki di Visual Studio 2019.
Menghapus
std::launder
juga tampaknya memperbaiki kerusakan:sumber
std::launder
diketahui / dilaksanakan secara tidak benar oleh beberapa implementasi. Mungkin versi MSVS Anda didasarkan pada implementasi yang salah. Sayangnya, saya tidak memiliki sumbernya.