İlkel static_vector uygulamasında tanımlanmamış olası davranış


12

tl; dr: Sanırım static_vector'um tanımlanmamış bir davranışa sahip, ama bulamıyorum.

Bu sorun, Microsoft Visual C ++ 17 üzerinde. Bu basit ve bitmemiş static_vector uygulama, yani yığın tahsis edilebilir sabit kapasiteli bir vektör var. Bu std :: aligned_storage ve std :: launder kullanan bir C ++ 17 programıdır. Soruyla alakalı olduğunu düşündüğüm bölümlere aşağıda kaynatmaya çalıştım:

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

Bu bir süre iyi sonuç verdi. Sonra, bir noktada, buna çok benzer bir şey yapıyordum - gerçek kod daha uzun, ama bu onun özüne varıyor:

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

Başka bir deyişle, ilk önce 8 baytlık Foobar yapılarını yığındaki bir static_vector'a kopyalarız, daha sonra birinci üye olarak 8 baytlık bir static_vector std :: çifti ve ikinci olarak uint64_t yaparız. Çifti oluşturulmadan hemen önce valuOnTheStack'ın doğru değerleri içerdiğini doğrulayabilirim. Ve ... bu segment çifti kurarken static_vector'ın kopya yapıcısında (çağıran işlevin içine yerleştirilmiş) etkinleştirilmiş optimizasyona sahip.

Uzun lafın kısası, demontajı kontrol ettim. İşlerin biraz garipleştiği yer; satır içi kopya yapıcısı etrafında oluşturulan asm aşağıda gösterilmiştir - bunun gerçek koddan, yukarıdaki örnekten değil, oldukça yakın ancak çift yapısının üzerinde daha fazla şey olduğuna dikkat edin:

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)  

Tamam, bu yüzden önce sayım elemanını kaynaktan hedefe kopyalayan iki mov komutumuz var; çok uzak çok iyi. edx, döngü değişkeni olduğu için sıfırlanır. Daha sonra sayımın sıfır olup olmadığını hızlıca kontrol ederiz; sıfır değil, bu nedenle for-döngüsüne geçiyoruz, burada 8 baytlık yapıyı önce 32 bitlik iki işlem kullanarak bellekten kayda, sonra kayıttan belleğe kopyalıyoruz. Ama balık gibi bir şey var - burada [ebp + edx * 8 +] gibi bir şeyden bir mov'un kaynak nesneden okunmasını bekleriz, bunun yerine sadece ... [ecx]. Kulağa doğru gelmiyor. Ecx'in değeri nedir?

Görünen o ki, ecx sadece bir çöp adresi içeriyor, aynı segfatasyon yaptığımız adres. Bu değeri nereden aldı? İşte hemen yukarıdaki 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)]  

Bu normal bir eski cdecl işlev çağrısı gibi görünüyor. Aslında, fonksiyonun hemen üstünde harici bir C fonksiyonuna çağrı vardır. Ama ne olduğuna dikkat edin: ecx, yığında argümanları itmek için geçici bir kayıt olarak kullanılıyor, işlev çağrılıyor ve ... sonra ecx, static_vector kaynağından okumak için yanlışlıkla kullanılıncaya kadar bir daha asla dokunulmuyor.

Uygulamada, ecx içeriğinin üzerine, burada adı verilen işlevle yazılır, ki bu elbette yapmasına izin verilir. Ancak, olmasa bile, ecx'in burada doğru olana bir adres içermesinin bir yolu yoktur - en iyi durumda, static_vector olmayan yerel bir yığın üyesine işaret eder. Derleyici sahte bir derleme yaymış gibi görünüyor. Bu işlev asla doğru çıktıyı üretemez.

İşte şimdi ben buradayım. Std :: launder arazi oyun oynarken optimizasyon etkinleştirildiğinde garip montaj bana tanımsız davranış gibi kokuyor. Ama bunun nereden gelebileceğini göremiyorum. Tamamlayıcı fakat marjinal olarak yararlı bilgi olarak, doğru bayraklara sahip clang, buna benzer montaj üretir, ancak değerleri okumak için ecx yerine ebp + edx'i doğru kullanır.


Sadece lanetli bir bakış ama neden aradığınız clear()kaynakları std::movearıyorsun?
Bathsheba

Bunun ne kadar alakalı olduğunu anlamıyorum. Elbette, static_vector öğesini aynı boyutta ancak bir grup taşınmış nesne ile bırakmak da yasal olacaktır. Static_vector destructor yine de çalıştığında içerik imha edilir. Ama taşınan vektörü sıfır büyüklüğünde bırakmayı tercih ediyorum.
pjohansson

Hum. Ödeme notumun ötesinde. Bu iyi sorulduğu için bir oy verin ve dikkat çekebilir.
Bathsheba

Kodunuzla herhangi bir kilitlenme yeniden oluşturulamıyor (eksikliğinden dolayı derlenmemesine yardımcı olmuyor is_iterator) lütfen minimal tekrarlanabilir bir örnek
Alan Birtles

1
btw, bence burada bir sürü kod anlamsız. Yani, burada hiçbir yerde atama operatörü çağırmazsınız, bu yüzden örnekten kaldırılabilir
bartop

Yanıtlar:


6

Bence derleyici hatası var. Ekleme __declspec( noinline )için operator[]kazasında düzeltmek gibi görünüyor:

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

Hatayı Microsoft'a bildirmeyi deneyebilirsiniz, ancak hata zaten Visual Studio 2019'da düzeltilmiş gibi görünüyor.

Kaldırmak std::launderda çökmeyi gideriyor gibi görünüyor:

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

Diğer açıklamalarda da azalıyorum. Şu anki durumumuz ne kadar berbat olursa olsun, olanın bu olduğu makul görünüyor, bu yüzden bunu kabul edilen cevap olarak işaretleyeceğim.
pjohansson

Çamaşır temizliği giderilir mi? Çamaşır temizliği açıkça tanımsız bir davranış olacaktır! Garip.
pjohansson

@pjohansson'un std::launderbazı uygulamalar tarafından yanlış uygulandığı bilinmektedir. Belki MSVS sürümünüz bu yanlış uygulamaya dayanmaktadır. Maalesef kaynaklara sahip değilim.
Fureeish
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.