原始static_vector实现中可能的未定义行为


12

tl;博士:我认为我的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字节Foobar结构复制到堆栈上的static_vector中,然后将std :: pair和8字节结构的static_vector作为第一个成员,并将uint64_t作为第二个成员。我可以在构造该对之前立即验证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)  

好的,首先我们有两条mov指令,将计数成员从源复制到目标;到目前为止,一切都很好。edx被归零,因为它是循环变量。然后我们快速检查count是否为零;它不为零,因此我们进入for循环,在此循环中,我们使用两个32位mov操作首先从内存到寄存器,然后从寄存器到内存,复制8字节结构。但是有一点可疑-我们希望从[ebp + edx * 8 +]之类的内容中读取一个mov来从源对象中读取内容,而只是... [ecx]。听起来不对。ecx的价值是什么?

事实证明,ecx仅包含一个垃圾地址,即我们正在隔离的垃圾地址。它从何处获得此价值?这是紧接上面的组件:

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玩游戏时启用优化时,奇怪的汇编对我来说就像未定义的行为。但是我看不到那可能来自哪里。作为补充但微不足道的信息,带有正确标志的clang会产生与此类似的汇编,除了它正确地使用ebp + edx而不是ecx来读取值。


只是粗略地看,但是为什么要调用已调用clear()的资源std::move
Bathsheba

我不知道这有什么关系。当然,将static_vector保留为相同大小,但保留一堆移出的对象也是合法的。无论如何,当static_vector析构函数运行时,内容将被破坏。但是我更喜欢将移出的向量的大小保留为零。
pjohansson

哼。那就超出我的薪水等级了。提出要求,因为它要求很好,可能会引起注意。
Bathsheba

无法使用您的代码重现任何崩溃(由于缺少,因此无法编译is_iterator),请提供一个最小的可重现示例
Alan Birtles

1
顺便说一句,我认为很多代码在这里都无关紧要。我的意思是,您在这里没有在任何地方调用赋值运算符,因此可以将其从示例中删除
bartop

Answers:


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

std::launder已知@pjohansson 被某些实现错误地实现。也许您的MSVS版本基于该错误的实现。不幸的是,我没有消息来源。
Fureeish
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.