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.
is_iterator
). Fornisci un esempio riproducibile minimo
clear()
le risorse su cui hai chiamatostd::move
?