기본 static_vector 구현에서 정의되지 않은 동작 std::destroy_at(std::addressof((*this)[count – 1]));

tl; dr : 내 static_vector에 정의되지 않은 동작이 있다고 생각하지만 찾을 수 없습니다.

이 문제는 Microsoft Visual C ++ 17에서 발생합니다.이 간단하고 완료되지 않은 static_vector 구현, 즉 스택 할당이 가능한 고정 용량을 가진 벡터가 있습니다. 이것은 std :: aligned_storage 및 std :: launder를 사용하는 C ++ 17 프로그램입니다. 문제와 관련이 있다고 생각되는 부분으로 아래에서 정리하려고했습니다.

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 바이트 Foobar 구조체를 스택의 static_vector에 복사 한 다음 8 바이트 구조체의 static_vector의 std :: pair를 첫 번째 멤버로 만들고 uint64_t를 두 번째 멤버로 만듭니다. valuesOnTheStack에 쌍이 생성되기 직전에 올바른 값이 포함되어 있는지 확인할 수 있습니다. 그리고 …이 segfault는 쌍을 구성 할 때 static_vector의 복사 생성자 (호출 함수에 인라인 됨) 내에서 최적화가 활성화되어 있습니다.

간단히 말해, 분해를 검사했습니다. 상황이 조금 이상해집니다. 인라인 복사 생성자 주위에 생성 된 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 명령이 있습니다. 여태까지는 그런대로 잘됐다. edx는 루프 변수이기 때문에 0이됩니다. 카운트가 0인지 확인합니다. 0이 아니므로 for 루프로 진행하여 먼저 메모리에서 레지스터로, 레지스터에서 메모리로 두 개의 32 비트 mov 연산을 사용하여 8 바이트 구조체를 복사합니다. 그러나 비린내가 있습니다. [ebp + edx * 8 +]와 같은 소스에서 소스 객체를 읽을 수있는 대신 ecx가 있습니다. 소리가 잘 들리지 않습니다. ecx의 가치는 무엇입니까?

ecx에는 가비지 주소가 포함되어 있으며, 우리가 segfaulting하는 것과 동일합니다. 이 값은 어디서 얻었습니까? 바로 위의 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는 스택에 인수를 푸시하는 임시 레지스터로 사용되며 함수가 호출됩니다 … 그런 다음 estatic은 소스 static_vector에서 읽기 위해 아래에서 잘못 사용될 때까지 다시 만지지 않습니다.

실제로 ecx의 내용은 여기에서 호출되는 함수로 덮어 쓰게되며 물론 허용됩니다. 그러나 그렇지 않은 경우에도 ecx가 올바른 것에 대한 주소를 포함 할 방법은 없습니다. 최대는 static_vector가 아닌 로컬 스택 멤버를 가리킬 것입니다. 마치 컴파일러가 가짜 어셈블리를 생성 한 것처럼 보입니다. 이 기능은 올바른 출력을 생성 할 수 없습니다 .

그것이 바로 내가 지금있는 곳입니다. std :: launder land에서 놀면서 최적화가 활성화되면 이상한 어셈블리가 정의되지 않은 동작처럼 나에게 냄새가납니다. 그러나 나는 그것이 어디에서 올 수 있는지 알 수 없습니다. 보충적이지만 아주 유용한 정보 인 올바른 플래그가있는 clang은 ecx 대신 ebp + edx를 사용하여 값을 읽는다는 점을 제외하면 이와 유사한 어셈블리를 생성합니다.



답변

컴파일러 버그가 있다고 생각합니다. 추가 __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 ] ) ); }


답변