พฤติกรรมที่ไม่ได้กำหนดเป็นไปได้ในการใช้งาน static_vector ดั้งเดิม


12

tl; dr: ฉันคิดว่า static_vector ของฉันมีพฤติกรรมที่ไม่ได้กำหนด แต่ฉันหามันไม่พบ

ปัญหานี้เกิดขึ้นกับ Microsoft Visual C ++ 17 ฉันมีการใช้งาน static_vector ที่เรียบง่ายและไม่สมบูรณ์เช่นเวกเตอร์ที่มีความจุคงที่ที่สามารถจัดสรรสแต็กได้ นี่คือโปรแกรม C ++ 17 โดยใช้ std :: aligned_storage และ std :: launder ฉันพยายามต้มลงไปด้านล่างเพื่อชิ้นส่วนที่ฉันคิดว่าเกี่ยวข้องกับปัญหา:

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;
};

ดูเหมือนว่าจะทำงานได้ดีในขณะที่ จากนั้นมาถึงจุดหนึ่งฉันได้ทำสิ่งที่คล้ายกันมาก - รหัสจริงอีกต่อไป แต่สิ่งนี้ได้รับส่วนสำคัญของมัน:

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 };
}

กล่าวอีกนัยหนึ่งเราคัดลอก 8-byte Foobar structs ไปเป็น static_vector บน stack จากนั้นเราสร้าง std :: pair ของ static_vector 8-byte structs เป็นสมาชิกคนแรกและ uint64_t เป็นวินาที ฉันสามารถตรวจสอบว่าค่า OnTheStack มีค่าที่ถูกต้องทันทีก่อนที่ทั้งคู่จะถูกสร้างขึ้น และ ... เซกค่าเริ่มต้นพร้อมการปรับให้เหมาะสมเปิดใช้งานภายในตัวสร้างสำเนาของ static_vector (ซึ่งถูก inlined ในฟังก์ชันการเรียก) เมื่อสร้างคู่

เรื่องสั้นสั้นฉันตรวจสอบการถอด นี่เป็นสิ่งที่แปลกไปเล็กน้อย asm ที่สร้างขึ้นรอบ ๆ ตัวสร้างการคัดลอกแบบอินไลน์แสดงไว้ด้านล่าง - โปรดทราบว่านี่คือจากรหัสจริงไม่ใช่ตัวอย่างด้านบนซึ่งค่อนข้างใกล้ แต่มีบางสิ่งที่เหนือการสร้างคู่:

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)  

เอาล่ะก่อนอื่นเรามีคำแนะนำ mov สองคำที่คัดลอกสมาชิก count จากต้นทางไปยังปลายทาง ดีมาก edx เป็นศูนย์เพราะมันเป็นตัวแปรลูป จากนั้นเราจะตรวจสอบอย่างรวดเร็วว่าการนับเป็นศูนย์หรือไม่ มันไม่เป็นศูนย์ดังนั้นเราจึงไปที่ for for loop ที่เราคัดลอกโครงสร้าง 8 ไบต์โดยใช้การดำเนินการ mov 32- บิตสองครั้งแรกจากหน่วยความจำเพื่อลงทะเบียนจากนั้นลงทะเบียนจากหน่วยความจำ แต่มีบางสิ่งบางอย่างคาว - ที่เราคาดว่า mov จากบางอย่างเช่น [ebp + edx * 8 +] เพื่ออ่านจากวัตถุต้นฉบับมีเพียง ... [ecx] นั่นไม่ได้เสียงที่ถูกต้อง ค่าของ ecx คืออะไร?

กลับกลายเป็นว่า ecx มีที่อยู่อีเมลขยะซึ่งเป็นที่อยู่เดียวกับที่เราทำการแยก มันได้รับค่านี้จากที่ไหน นี่คือ asm ด้านบนทันที:

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)]  

ดูเหมือนว่าการเรียกฟังก์ชัน cdecl แบบเก่าปกติ แน่นอนฟังก์ชั่นนั้นมีการเรียกไปยังฟังก์ชัน C ภายนอกที่อยู่ด้านบน แต่ให้สังเกตว่ามีอะไรเกิดขึ้น: ecx กำลังถูกใช้เป็นรีจิสเตอร์ชั่วคราวเพื่อส่งอาร์กิวเมนต์บนสแต็กฟังก์ชันจะถูกเรียกใช้และ ... จากนั้นจะไม่สัมผัส ecx อีกเลยจนกว่าจะมีการใช้ด้านล่างอย่างผิดพลาดเพื่ออ่านจาก static_vector ต้นทาง

ในทางปฏิบัติเนื้อหาของ ecx ถูกเขียนทับโดยฟังก์ชันที่เรียกว่าที่นี่ซึ่งแน่นอนว่าอนุญาตให้ทำ แต่แม้ว่ามันจะไม่เป็นเช่นนั้นก็ไม่มีทางที่ ecx จะมีที่อยู่ไปยังสิ่งที่ถูกต้องที่นี่ - ที่ดีที่สุดมันจะชี้ไปที่สมาชิกสแต็คท้องถิ่นที่ไม่ใช่ static_vector ดูเหมือนว่าคอมไพเลอร์ได้เปิดตัวชุดประกอบปลอม ฟังก์ชันนี้ไม่สามารถสร้างผลลัพธ์ที่ถูกต้องได้

นั่นคือสิ่งที่ฉันอยู่ตอนนี้ แอสเซมบลีแปลก ๆ เมื่อเปิดใช้งานการปรับให้เหมาะสมในขณะที่เล่นใน std :: launder land มีกลิ่นให้ฉันเหมือนพฤติกรรมที่ไม่ได้กำหนด แต่ฉันไม่สามารถดูได้ว่าจะมาจากไหน ในฐานะที่เป็นข้อมูลเพิ่มเติม แต่มีประโยชน์เล็กน้อยเสียงดังกราวที่มีธงด้านขวาสร้างชุดประกอบที่คล้ายคลึงกับสิ่งนี้ยกเว้นว่าจะใช้ ebp + edx อย่างถูกต้องแทน ecx เพื่ออ่านค่า


ดูคร่าวๆ แต่ทำไมคุณถึงเรียกหาclear()ทรัพยากรที่คุณโทรมาstd::move?
Bathsheba

ฉันไม่เห็นว่ามันเกี่ยวข้องอย่างไร แน่นอนว่ามันจะถูกกฎหมายหากปล่อยให้ static_vector มีขนาดเท่ากัน แต่มีวัตถุที่ถูกย้ายออกไปจำนวนมาก เนื้อหาจะถูกทำลายเมื่อ static_vector destructor ทำงานอยู่ แต่ฉันชอบที่จะปล่อยให้เวกเตอร์ที่ย้ายแล้วมีขนาดเป็นศูนย์
pjohansson

ครวญเพลง เกินเกรดที่จ่ายไปแล้ว มี upvote เช่นนี้เป็นอย่างดีถามและอาจดึงดูดความสนใจ
Bathsheba

ไม่สามารถทำซ้ำความผิดพลาดใด ๆ กับรหัสของคุณ (ไม่ช่วยโดยไม่รวบรวมเนื่องจากขาดis_iterator) โปรดให้ตัวอย่างที่ทำซ้ำได้น้อยที่สุด
Alan Birtles

1
btw ฉันคิดว่ารหัสจำนวนมากไม่เกี่ยวข้องที่นี่ ฉันหมายความว่าคุณไม่ได้โทรหาผู้ดำเนินการมอบหมายที่ใดก็ได้ที่นี่ดังนั้นจึงสามารถลบออกได้จากตัวอย่าง
bartop

คำตอบ:


6

ฉันคิดว่าคุณมีข้อผิดพลาดในการรวบรวม การเพิ่ม__declspec( noinline )ไปยังoperator[]ดูเหมือนจะแก้ไขข้อผิดพลาด:

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

คุณสามารถลองรายงานข้อผิดพลาดไปยัง Microsoft แต่ข้อผิดพลาดที่ดูเหมือนจะแก้ไขแล้วใน Visual Studio 2019

การลบstd::launderยังดูเหมือนว่าจะแก้ไขความผิดพลาด:

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

ฉันมีคำอธิบายอื่น ๆ น้อยเกินไป เท่าที่ดูดเมื่อสถานการณ์ปัจจุบันของเราดูเหมือนว่าเป็นไปได้ว่านี่คือสิ่งที่เกิดขึ้นดังนั้นฉันจะทำเครื่องหมายว่าเป็นคำตอบที่ยอมรับได้
pjohansson

การลบการฟอกเป็นการแก้ไขหรือไม่ การลบการฟอกจะเป็นพฤติกรรมที่ไม่ได้กำหนดอย่างชัดเจน! แปลก.
pjohansson

@pjohansson std::launderเป็นที่รู้จักกัน / จะถูกนำไปใช้อย่างไม่ถูกต้องโดยการใช้งานบางอย่าง บางทีเวอร์ชัน MSVS ของคุณอาจขึ้นอยู่กับการใช้งานที่ไม่ถูกต้อง ฉันไม่ได้มีแหล่งที่มาโชคไม่ดี
Fureeish
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.