Possibile comportamento indefinito nell'implementazione primitiva static_vector


12

tl; dr: penso che il mio static_vector abbia un comportamento indefinito, ma non riesco a trovarlo.

Questo problema si verifica in Microsoft Visual C ++ 17. Ho questa semplice e incompleta implementazione static_vector, cioè un vettore con una capacità fissa che può essere allocata in pila. Questo è un programma C ++ 17, usando std :: align_storage e std :: launder. Ho provato a ridurlo in basso alle parti che ritengo rilevanti per il problema:

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

Questo sembrava funzionare bene per un po '. Quindi, a un certo punto, stavo facendo qualcosa di molto simile a questo: il codice attuale è più lungo, ma questo ne prende l'essenza:

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

In altre parole, prima copiamo le strutture Foobar a 8 byte in un static_vector sullo stack, quindi creiamo una coppia std :: coppia di un static_vector di strutture a 8 byte come primo membro e un uint64_t come secondo. Posso verificare che valueOnTheStack contenga i giusti valori immediatamente prima della costruzione della coppia. E ... questo segfault con l'ottimizzazione abilitata all'interno del costruttore di copie static_vector (che è stato integrato nella funzione di chiamata) durante la costruzione della coppia.

Per farla breve, ho ispezionato lo smontaggio. Questo dove le cose si fanno un po 'strane; l'asm generato attorno al costruttore di copia in linea è mostrato sotto - nota che questo proviene dal codice reale, non dall'esempio sopra, che è abbastanza vicino ma ha alcune cose sopra la costruzione della coppia:

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)  

Ok, quindi prima abbiamo due istruzioni mov che copiano il membro count dalla sorgente alla destinazione; Fin qui tutto bene. edx è azzerato perché è la variabile loop. Abbiamo quindi un controllo rapido se il conteggio è zero; non è zero, quindi procediamo al ciclo for dove copiamo la struttura a 8 byte usando due operazioni di mov a 32 bit prima dalla memoria al registro, quindi dal registro alla memoria. Ma c'è qualcosa di sospetto - dove ci aspetteremmo un movimento da qualcosa come [ebp + edx * 8 +] per leggere dall'oggetto sorgente, c'è invece solo ... [ecx]. Non suona bene. Qual è il valore di ecx?

Si scopre che ecx contiene solo un indirizzo di immondizia, lo stesso su cui stiamo segfaultando. Da dove ha preso questo valore? Ecco l'asm immediatamente sopra:

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

Sembra una normale chiamata di funzione cdecl. In effetti, la funzione ha una chiamata a una funzione C esterna appena sopra. Ma nota cosa sta succedendo: ecx viene utilizzato come registro temporaneo per inviare argomenti nello stack, la funzione viene invocata e ... quindi ecx non viene mai più toccato fino a quando non viene erroneamente utilizzato di seguito per leggere dal sorgente static_vector.

In pratica, i contenuti di ecx vengono sovrascritti dalla funzione chiamata qui, che ovviamente è consentita. Ma anche se così non fosse, non è possibile che ecx contenga mai un indirizzo per la cosa corretta qui - nella migliore delle ipotesi, indicherebbe un membro dello stack locale che non sia static_vector. Sembra che il compilatore abbia emesso un assemblaggio fasullo. Questa funzione non potrebbe mai produrre l'output corretto.

Ecco dove sono adesso. Strano assemblaggio quando le ottimizzazioni sono abilitate mentre si gioca in std :: launder land mi odora come un comportamento indefinito. Ma non riesco a vedere da dove possa provenire. Come informazione supplementare ma marginalmente utile, clang con i flag giusti produce un assemblaggio simile a questo, tranne che usa correttamente ebp + edx invece di ecx per leggere i valori.


Solo uno sguardo superficiale, ma perché stai invocando clear()le risorse su cui hai chiamato std::move?
Bathsheba,

Non vedo quanto sia rilevante. Certo, sarebbe anche legale lasciare static_vector con le stesse dimensioni ma un gruppo di oggetti spostati. Il contenuto verrà distrutto quando il distruttore static_vector viene eseguito comunque. Ma preferisco lasciare il vettore spostato con una dimensione pari a zero.
Pjohansson,

Ronzio. Al di là del mio voto di paga allora. Avere un voto in quanto è ben richiesto e potrebbe attirare l'attenzione.
Bathsheba,

Impossibile riprodurre alcun arresto anomalo con il codice (non aiutato dal fatto che non viene compilato a causa della mancanza di is_iterator). Fornisci un esempio riproducibile minimo
Alan Birtles

1
a proposito, penso che un sacco di codice sia irrilevante qui. Voglio dire, non si chiama operatore di assegnazione da nessuna parte qui in modo che possa essere rimosso dall'esempio
bartop

Risposte:


6

Penso che tu abbia un bug del compilatore. L'aggiunta __declspec( noinline )a operator[]sembra che risolva il crash:

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

Puoi provare a segnalare il bug a Microsoft ma il bug sembra essere già stato risolto in Visual Studio 2019.

La rimozione std::laundersembra anche risolvere il crash:

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

Sto esaurendo anche altre spiegazioni. Per quanto possa fare schifo data la nostra situazione attuale, sembra plausibile che questo sia ciò che sta succedendo, quindi lo segnerò come risposta accettata.
Pjohansson,

La rimozione del riciclaggio lo risolve? Rimuovere il riciclaggio sarebbe esplicitamente un comportamento indefinito! Strano.
pjohansson,

@pjohansson std::launderè / era noto per essere stato implementato in modo errato da alcune implementazioni. Forse la tua versione di MSVS si basa su un'implementazione errata. Sfortunatamente non ho le fonti.
Fureeish,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.