un comportamiento indefinido posible en aplicación static_vector primitiva

pjohansson:

tl; dr: Creo que mi static_vector tiene un comportamiento indefinido, pero no puedo encontrarlo.

Este problema está en Microsoft Visual C ++ 17. Tengo esta aplicación sencilla y static_vector sin terminar, es decir, un vector con una capacidad fija que puede ser asignada pila. Esto es un 17 programa C ++, utilizando std :: aligned_storage y std :: artesa. He tratado de reducirlo por debajo de las piezas que creo que son pertinentes a la cuestió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;
};

Esto pareció bien el trabajo por un tiempo. Entonces, en un momento dado, que estaba haciendo algo muy similar a esto - el código real es más largo, pero esto nos lleva al quid de la cuestió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 };
}

En otras palabras, la primera copia de 8 bytes estructuras Foobar en un static_vector en la pila, entonces hacemos un std :: par de un static_vector de estructuras de 8 bytes como el primer miembro, y una uint64_t como el segundo. Puedo verificar que valuesOnTheStack contiene los valores correctos inmediatamente antes se construye el par. Y ... segfaults con optimización permitieron constructor de copia de static_vector el interior (que ha sido inline en la función de llamada) en la construcción de la pareja.

Cuento largo, inspeccioné el desmontaje. Aquí es donde las cosas se ponen un poco raro; el asm generado en torno al constructor de copia inline se muestra a continuación - Tenga en cuenta que esto es a partir del código actual, no la muestra anterior, lo cual es bastante estrecha, pero tiene algunas cosas más por encima de la construcción de par:

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)  

Está bien, así que primero tenemos dos instrucciones mov copiar el miembro recuento desde la fuente hasta el destino; hasta ahora tan bueno. EDX se pone a cero porque es la variable de bucle. Tenemos entonces una comprobación rápida si el recuento es cero; no es cero, por lo que se procede a la de bucle donde se copia la estructura de 8 bytes utilizando dos operaciones de 32 bits mov primero de la memoria para registrar, a continuación, desde el registro en la memoria. Pero hay algo a pescado - donde esperaríamos un mov de algo como [ebp + EDX * 8 +] leer del objeto de origen, no hay lugar sólo ... [ecx]. Eso no suena bien. ¿Cuál es el valor de ECX?

Resulta que, ecx solo contiene una dirección de basuras, el mismo que estamos en violación de segmento. ¿De dónde obtener este valor de? Aquí está la asm inmediatamente superior:

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

Esto parece ser una llamada a la función cdecl regulares de edad. De hecho, la función tiene una llamada a una función externa C justo por encima. Pero tenga en cuenta lo que está pasando: ECX está siendo utilizado como un registro temporal a los argumentos de empuje en la pila, se invoca la función, y luego ... ecx no se toca de nuevo hasta que se utiliza erróneamente a continuación para leer desde la fuente static_vector.

En la práctica, el contenido de ecx se sobreescriben por la función llamada aquí, que por supuesto es permitido hacerlo. Pero incluso si no fuera así, no hay manera de ECX es cada vez va a contener una dirección a lo correcto aquí - en el mejor, sería apuntar a un miembro de la pila local que no es la static_vector. Parece como si el compilador ha emitido algunos falsos montaje. Esta función podría no producir el resultado correcto.

Así que ahí es donde estoy ahora. Asamblea raro cuando se habilitan optimizaciones, mientras que jugando en la tierra std :: artesa huele a mí como un comportamiento indefinido. Pero no puedo ver a dónde puede provenir de. Como información complementaria, pero marginalmente útil, sonido metálico con las banderas correctas produce el montaje similar a este, excepto que usa correctamente ebp + EDX en lugar de ecx para leer los valores.

Alan Birtles:

Yo creo que hay un error del compilador. Añadiendo __declspec( noinline )a operator[]parece solucionar el accidente:

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

Usted puede tratar de informar del error a Microsoft, pero el fallo parece estar ya solucionado en Visual Studio 2019.

Extracción std::laundertambién parece solucionar el accidente:

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

Supongo que te gusta

Origin http://43.154.161.224:23101/article/api/json?id=369405&siteId=1
Recomendado
Clasificación