может по-прежнему «знать» индекс другими способами (например, подсчитывать количество его вызовов). Да, это был бы ужасный код. Но семантически это верно без UB, и компилятор должен уважать это.

аюсь создать локальный массив некоторых значений POD (например,double) с фиксированнойmax_size что известно во время компиляции, затем прочитайте среду выполненияsize стоимость (size <= max_size) и обработать первымsize элементы из этого массива.

Вопрос в том, почему компилятор не исключает чтение и запись в стек, когдаarr а такжеsize помещены в одно и то жеstruct/class, в отличие от случая, когдаarr а такжеsize такое независимые локальные переменные?

Вот мой код:

#include <cstddef>
constexpr std::size_t max_size = 64;

extern void process_value(double& ref_value);

void test_distinct_array_and_size(std::size_t size)
{
    double arr[max_size];
    std::size_t arr_size = size;

    for (std::size_t i = 0; i < arr_size; ++i)
        process_value(arr[i]);
}

void test_array_and_size_in_local_struct(std::size_t size)
{
    struct
    {
        double arr[max_size];
        std::size_t size;
    } array_wrapper;
    array_wrapper.size = size;

    for (std::size_t i = 0; i < array_wrapper.size; ++i)
        process_value(array_wrapper.arr[i]);
}

Сборочный вывод дляtest_distinct_array_and_size из Clang с -O3:

test_distinct_array_and_size(unsigned long): # @test_distinct_array_and_size(unsigned long)
  push r14
  push rbx
  sub rsp, 520
  mov r14, rdi
  test r14, r14
  je .LBB0_3
  mov rbx, rsp
.LBB0_2: # =>This Inner Loop Header: Depth=1
  mov rdi, rbx
  call process_value(double&)
  add rbx, 8
  dec r14
  jne .LBB0_2
.LBB0_3:
  add rsp, 520
  pop rbx
  pop r14
  ret

Сборочный вывод дляtest_array_and_size_in_local_struct:

test_array_and_size_in_local_struct(unsigned long): # @test_array_and_size_in_local_struct(unsigned long)
  push r14
  push rbx
  sub rsp, 520
  mov qword ptr [rsp + 512], rdi
  test rdi, rdi
  je .LBB1_3
  mov r14, rsp
  xor ebx, ebx
.LBB1_2: # =>This Inner Loop Header: Depth=1
  mov rdi, r14
  call process_value(double&)
  inc rbx
  add r14, 8
  cmp rbx, qword ptr [rsp + 512]
  jb .LBB1_2
.LBB1_3:
  add rsp, 520
  pop rbx
  pop r14
  ret

Последние компиляторы GCC и MSVC делают в основном то же самое с чтением и записью стека.

Как мы видим, читает и пишет вarray_wrapper.size переменные в стеке не оптимизируются в последнем случае. Есть запись оsize ценность в местоположение[rsp + 512] до начала цикла, и чтение из этого места послекаждый итерация.

Итак, компилятор ожидает, что мы захотим изменитьarray_wrapper.size изprocess_value(array_wrapper.arr[i]) вызов (взяв адрес текущего элемента массива и применив к нему некоторые странные смещения?)

Но если мы попытаемся сделать это с помощью этого звонка, разве это не будет неопределенным поведением?

Когда мы переписываем цикл следующим образом

for (std::size_t i = 0, sz = array_wrapper.size; i < sz; ++i)
    process_value(array_wrapper.arr[i]);

эти ненужные чтения в конце каждой итерации исчезнут. Но начальная запись в[rsp + 512] останется, это означает, что компилятор все еще ожидает, что мы сможем получить доступ кarray_wrapper.size переменная в этом месте из этихprocess_value звонки (делая некоторую странную магию, основанную на смещении).

Почему?

Является ли это небольшим недостатком в реализации современных компиляторов (который, надеюсь, будет исправлен в ближайшее время)? Или стандарт C ++ действительно требует такого поведения, которое приводит к генерации менее эффективного кода всякий раз, когда мы помещаем массив и его размер в один и тот же класс?

Постскриптум

Я понимаю, что мой пример кода выше может показаться немного надуманным. Но учтите это: я хотел бы использовать легкийboost::container::static_vector-подобный шаблон класса в моем коде для более безопасных и удобных манипуляций в стиле "C ++" с псевдодинамическими массивами элементов POD. Так что мойPODVector будет содержать массив иsize_t в том же классе:

template<typename T, std::size_t MaxSize>
class PODVector
{
    static_assert(std::is_pod<T>::value, "T must be a POD type");

private:
    T _data[MaxSize];
    std::size_t _size = 0;

public:
    using iterator = T *;

public:
    static constexpr std::size_t capacity() noexcept
    {
        return MaxSize;
    }

    constexpr PODVector() noexcept = default;

    explicit constexpr PODVector(std::size_t initial_size)
        : _size(initial_size)
    {
        assert(initial_size <= capacity());
    }

    constexpr std::size_t size() const noexcept
    {
        return _size;
    }

    constexpr void resize(std::size_t new_size)
    {
        assert(new_size <= capacity());
        _size = new_size;
    }

    constexpr iterator begin() noexcept
    {
        return _data;
    }

    constexpr iterator end() noexcept
    {
        return _data + _size;
    }

    constexpr T & operator[](std::size_t position)
    {
        assert(position < _size);
        return _data[position];
    }
};

Использование:

void test_pod_vector(std::size_t size)
{
    PODVector<double, max_size> arr(size);

    for (double& val : arr)
        process_value(val);
}

Если описанная выше проблема действительно вызвана стандартом C ++ (и не является ошибкой авторов компиляторов), то такиеPODVector никогда не будет столь же эффективным, как необработанное использование массива и «не связанной» переменной для размера. И это было бы очень плохо для C ++ как языка, который требует абстракций без накладных расходов.

Ответы на вопрос(1)

Ваш ответ на вопрос