プリミティブstatic_vector実装の可能性未定義の動作

pjohansson:

TL; DR:私は私のstatic_vectorは未定義の動作を持っていると思うが、私はそれを見つけることができません。

この問題は、Microsoft Visual C ++私は、スタックに割り当てることができる固定容量で、このシンプルで未完成static_vectorの実装、すなわちAのベクトルを持っている17の上にあります。これは、STD :: aligned_storageとstd ::樋を使用して、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バイトの構造体のstatic_vectorのはstd ::ペア、第二などuint64_tをを作り、スタックにstatic_vectorに8バイトFOOBARの構造体をコピーしてください。私は、ペアが構築される前にvaluesOnTheStackはすぐに正しい値が含まれていることを確認することができます。ペアを構築する際に...最適化して、このセグメンテーション違反は(呼び出し元の関数にインライン化された)内部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)  

わかりましたので、最初に我々は、送信元から宛先までのカウントメンバーのコピー2つの、MOV命令を持っています。ここまでは順調ですね。それはループ変数だからEDXがゼロにされます。カウントがゼロであれば私たちは、その後、簡単なチェックを持っています。それがゼロではないので、我々は次に、レジスタからメモリに、メモリからレジスタに最初の2つの32ビットMOV操作を使用して8バイトの構造体をコピーするforループに進みます。しかし、そこの生臭い何か - 私たちは[EBP + EDX * 8 +]ソースオブジェクトから読むのが好きなものからMOVを期待する場合には、だけではなく、そこにある... [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は、スタック上のプッシュ引数への一時レジスタとして使用されている、関数が呼び出され、そして...誤ったソースstatic_vectorから読み取るために以下で使用されるまで、ECX再びタッチされることはありません。

実際には、当然のことながら行うことを許可され、ここで呼び出された関数によるECX GET上書きの内容。しかし、それはしなかった場合でも、これまでここで正しいものにアドレスが含まれているために起こっているECX方法はありません-最高の状態で、それはstatic_vectorないローカルスタックメンバーを指します。コンパイラは、いくつかの偽のアセンブリを放射されたかのように思えます。この関数は、可能性が決して正しい出力を生成しません。

私は今の私ことですので。std ::樋地で遊んでいる間最適化が有効になっている奇妙なアセンブリは、未定義の動作のように私にはにおいがします。しかし、私はそれがから来ることができた場所を確認することはできません。それは値を正しく読み取るためにEBP + EDXの代わりに、ECXを使用する以外の補足が、わずかに有用な情報として、右旗と打ち鳴らすが、これに類似したアセンブリを生成します。

アランBirtles:

私はあなたがコンパイラのバグを持っていると思います。追加__declspec( noinline )するにはoperator[]クラッシュを修正しているようです。

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

あなたはマイクロソフトにバグを報告試すことができますが、バグがすでにVisual Studioの2019で修正されているようです。

削除std::launderもすると、クラッシュを修正しているようです。

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

おすすめ

転載: http://43.154.161.224:23101/article/api/json?id=369395&siteId=1