Hành vi không xác định có thể có trong triển khai static_vector nguyên thủy


12

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ị.


Chỉ có một cái nhìn cursory nhưng tại sao bạn gọi clear()vào các tài nguyên mà bạn đã gọi std::move?
Bathsheba

Tôi không thấy nó liên quan như thế nào. Chắc chắn, nó cũng sẽ là hợp pháp khi rời khỏi static_vector với cùng kích thước nhưng một loạt các đối tượng chuyển đi. Các nội dung sẽ bị hủy khi trình hủy bỏ static_vector chạy. Nhưng tôi thích để lại vectơ di chuyển với kích thước bằng không.
pjohansson

Hum. Vượt quá mức lương của tôi rồi. Có một upvote vì điều này được yêu cầu tốt, và có thể thu hút sự chú ý.
Bathsheba

Không thể tái tạo bất kỳ sự cố nào với mã của bạn (không được trợ giúp bởi nó không được biên dịch do thiếu is_iterator), vui lòng cung cấp một ví dụ có thể lặp lại tối thiểu
Alan Birtles

1
btw, tôi nghĩ rằng rất nhiều mã không liên quan ở đây. Ý tôi là, bạn không gọi toán tử gán ở bất cứ đâu tại đây để có thể xóa nó khỏi ví dụ
bartop

Câu trả lời:


6

Tôi nghĩ rằng bạn có một lỗi biên dịch. Thêm __declspec( noinline )vào operator[]dường như để khắc phục sự cố:

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

Bạn có thể thử báo cáo lỗi cho Microsoft nhưng lỗi dường như đã được sửa trong Visual Studio 2019.

Loại bỏ std::laundercũng có vẻ để khắc phục sự cố:

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

Tôi cũng sắp hết lời giải thích khác. Càng nhiều điều tệ hại với tình hình hiện tại của chúng tôi, có vẻ hợp lý rằng đây là những gì đang diễn ra, vì vậy tôi sẽ đánh dấu đây là câu trả lời được chấp nhận.
pjohansson

Loại bỏ giặt sửa chữa nó? Loại bỏ giặt là rõ ràng là hành vi không xác định! Lạ thật.
pjohansson

@pjohansson std::launderđược / được biết là được triển khai không chính xác bởi một số triển khai. Có thể phiên bản MSVS của bạn dựa trên việc triển khai không chính xác. Thật không may, tôi không có nguồn.
Fureeish
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.