tl; dr: Tôi nghĩ rằng static_vector của tôi có hành vi không xác định, nhưng tôi không thể tìm thấy nó.
Vấn đề này là trên Microsoft Visual C ++ 17. Tôi có triển khai static_vector đơn giản và chưa hoàn thành này, tức là một vectơ có dung lượng cố định có thể được phân bổ ngăn xếp. Đây là chương trình C ++ 17, sử dụng std :: căn_st Storage và std :: launder. Tôi đã cố gắng đưa nó xuống bên dưới đến những phần mà tôi nghĩ có liên quan đến vấn đề:
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;
};
Điều này xuất hiện để làm việc tốt trong một thời gian. Sau đó, tại một thời điểm, tôi đã làm một cái gì đó rất giống với điều này - mã thực tế dài hơn, nhưng điều này đạt được ý chính của nó:
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 };
}
Nói cách khác, trước tiên, chúng tôi sao chép các cấu trúc Foobar 8 byte vào một tệp tĩnh trên ngăn xếp, sau đó chúng tôi tạo một cặp std :: của một chuỗi tĩnh của các chuỗi 8 byte làm thành viên đầu tiên và một uint64_t làm thứ hai. Tôi có thể xác minh rằng giá trịOnTheStack chứa đúng giá trị ngay trước khi cặp được tạo. Và ... các phân tách này với tối ưu hóa được kích hoạt bên trong hàm tạo sao chép của static_vector (đã được đưa vào hàm gọi) khi xây dựng cặp.
Câu chuyện dài, tôi kiểm tra việc tháo gỡ. Đây là nơi mọi thứ trở nên hơi kỳ lạ; mã asm được tạo xung quanh hàm tạo sao chép nội tuyến được hiển thị bên dưới - lưu ý rằng đây là từ mã thực tế, không phải mẫu ở trên, khá gần nhưng có một số nội dung khác trên cấu trúc cặp:
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)
Được rồi, vì vậy trước tiên chúng tôi có hai hướng dẫn Mov sao chép thành viên đếm từ nguồn đến đích; càng xa càng tốt. edx bằng 0 vì đó là biến vòng lặp. Sau đó chúng tôi có một kiểm tra nhanh nếu số lượng bằng không; nó không phải là số không, vì vậy chúng tôi tiến hành vòng lặp for nơi chúng tôi sao chép cấu trúc 8 byte bằng hai thao tác Mov 32 bit trước tiên từ bộ nhớ để đăng ký, sau đó từ đăng ký sang bộ nhớ. Nhưng có điều gì đó đáng nghi - nơi chúng ta mong đợi một Mov từ thứ gì đó như [ebp + edx * 8 +] để đọc từ đối tượng nguồn, thay vào đó chỉ là ... [ecx]. Điều đó không đúng. Giá trị của ecx là gì?
Hóa ra, ecx chỉ chứa một địa chỉ rác, chính là địa chỉ mà chúng tôi đang phân tách. Nó lấy giá trị này từ đâu? Đây là mã asm ngay trên:
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)]
Điều này trông giống như một cuộc gọi chức năng cdecl cũ thông thường. Thật vậy, hàm có một lệnh gọi đến hàm C bên ngoài ngay phía trên. Nhưng lưu ý những gì đang xảy ra: ecx đang được sử dụng như một thanh ghi tạm thời để đẩy các đối số trên ngăn xếp, hàm được gọi và ... sau đó ecx không bao giờ được chạm lại cho đến khi nó được sử dụng sai dưới đây để đọc từ bộ giải mã nguồn.
Trong thực tế, nội dung của ecx bị ghi đè bởi hàm được gọi ở đây, điều này tất nhiên được phép làm. Nhưng ngay cả khi điều đó không xảy ra, không có cách nào ecx sẽ chứa một địa chỉ cho điều chính xác ở đây - tốt nhất, nó sẽ trỏ đến một thành viên ngăn xếp cục bộ không phải là static_vector. Có vẻ như trình biên dịch đã phát ra một số lắp ráp không có thật. Hàm này không bao giờ có thể tạo ra đầu ra chính xác.
Vì vậy, đó là nơi tôi đang ở. Lắp ráp kỳ lạ khi tối ưu hóa được kích hoạt trong khi chơi xung quanh trong std :: launder có mùi đối với tôi như hành vi không xác định. Nhưng tôi không thể thấy nơi đó có thể đến từ đâu. Là thông tin bổ sung nhưng rất hữu ích, clang với các cờ bên phải tạo ra sự lắp ráp tương tự như vậy, ngoại trừ nó sử dụng chính xác ebp + edx thay vì ecx để đọc các giá trị.
is_iterator
), vui lòng cung cấp một ví dụ có thể lặp lại tối thiểu
clear()
vào các tài nguyên mà bạn đã gọistd::move
?