Можлива невизначена поведінка в примітивній реалізації static_vector


12

tl; dr: Я думаю, що мій static_vector має невизначену поведінку, але я не можу його знайти.

Ця проблема стосується Microsoft Visual C ++ 17. У мене є така проста і незавершена реалізація static_vector, тобто вектор з фіксованою ємністю, який можна виділити стеком. Це програма C ++ 17, використовуючи std :: align_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 :: пара static_vector 8-байтових структур як перший член, а uint64_t як другий. Я можу переконатися, що valuesOnTheStack містить правильні значення безпосередньо перед побудовою пари. І ... цей сегментарний параметр з оптимізацією увімкнено всередині конструктора копій static_vector (який був вкладений у функцію виклику) при побудові пари.

Коротка історія, я перевірив розбирання. Тут дещо стає дивним; згенерований асм навколо вбудованого конструктора копії показано нижче - зауважте, що це з фактичного коду, а не зразка вище, який є досить близьким, але має ще кілька речей над парною конструкцією:

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 нульовий, тому що це змінна цикл. Потім у нас є швидка перевірка, чи є нуль; він не дорівнює нулю, тому ми переходимо до циклу for, куди ми копіюємо 8-байтну структуру, використовуючи дві 32-бітні операції mov спочатку з пам'яті для реєстрації, потім з регістра в пам'ять. Але є щось рибне - де ми б очікували, що mov із чогось на зразок [ebp + edx * 8 +] прочитати з вихідного об’єкта, натомість є просто ... [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. Дійсно, функція має виклик зовнішньої функції С трохи вище. Але зверніть увагу на те, що відбувається: ecx використовується як тимчасовий регістр для натискання аргументів на стек, функція викликається, і ... тоді ecx більше ніколи не торкається, поки помилково не використовується нижче для читання з джерела static_vector.

На практиці вміст ecx перезаписується функцією, що називається тут, що, звичайно, дозволяється робити. Але навіть якщо цього не сталося, немає ніякого способу, щоб ecx коли-небудь містив адресу правильної речі тут - у кращому випадку, це вказувало б на локального члена стека, який не є static_vector. Начебто компілятор випустив якусь хибну збірку. Ця функція ніколи не може дати правильний вихід.

Тож ось я зараз. Дивна збірка, коли ввімкнено оптимізацію під час гри в std :: launder land, пахне мені як невизначена поведінка. Але я не бачу, звідки це могло б походити. Як додаткова, але надзвичайно корисна інформація, clang з правими прапорами створює аналогічну збірці до цієї, за винятком того, що вона правильно використовує ebp + edx замість ecx для зчитування значень.


Тільки короткий погляд, але чому ти закликаєш clear()ресурси, на які ти дзвонив std::move?
Вірсавія

Я не бачу, наскільки це актуально. Звичайно, також було б законно залишити static_vector такого ж розміру, але купу переміщених об'єктів. Вміст буде знищено, коли деструктор static_vector запускається. Але я вважаю за краще залишити перенесений вектор з розміром нуля.
pjohansson

Гул. Окрім моєї зарплати тоді. Отримайте нагороду, оскільки це добре запитують, і це може привернути увагу.
Вірсавія

Неможливо відтворити будь-який збій із вашим кодом (не допомогли йому не скластись через відсутність is_iterator), будь ласка, надайте мінімально відтворюваний приклад
Alan Birtles

1
btw, я думаю, що багато коду тут не мають значення. Я маю на увазі, ви нікуди не дзвоните оператору призначення, щоб його можна було зняти з прикладу
bartop

Відповіді:


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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.