cpp-ru / ideas

Идеи по улучшению языка C++ для обсуждения
https://cpp-ru.github.io/proposals
Creative Commons Zero v1.0 Universal
88 stars 0 forks source link

Что-то вроде dealiasing_cast<T*>(T2*) для POD-массивов с одинаковой компоновкой (layout'ом) #485

Open topin89 opened 2 years ago

topin89 commented 2 years ago

Сама идея

Хочется иметь возможность явно говорить компилятору, что указатель на структуру

struct xy{
    int x;
    int y;
};

можно считать указателям на эту:

struct Coords{
    int column;
    int row;
};

Применение

Структуры вроде lib1::Color или lib2::Coords применяются во многих библиотеках, и как правило в них используются одни и те же типы, расположение и часто названия переменных. Это одни и те же структуры, и это одни и те же массивы структур. Но reinterpret_cast<Coords*>(xy_pointer) невозможен без нарушения Strict Aliasing. Следовательно, если результат одной библиотеки нужно передать как аргумент в другую, нужно сильно извернуться.

Это или копирование всего массива, или, если библиотека поддерживает итераторы, написание итератора-конвертера. Или забить и писать reinterpret_cast и надеятся на лучшее. В конце концов, в clang'е прокатывает даже

std::vector<long> a;
std::vector<double> &b = *reinterpret_cast< std::vector<double>* >(&a);

Конечно, кроме случаев, когда не прокатывает.

Если библиотека поддерживает std::span на вход, было бы возможно сделать

std::span<A> a{...};
std::span<B> b;
std::memcpy(a, b, sizeof(A));

но опять же, в момент взятия значения из B* Где-то в потрохах span'а, будет нарушения строгости и неопределённое поведение.

Ожидаемые проблемы

Само правило строгого разыменования взялось не от хорошей жизни. Тревис Доунс хорошо описал, а в PVS-Studio хорошо перевели, как отсутствие подобного ограничения влияет на банальный обход по вектору char'ов. И, если я правильно понял, сама возможность подобного преобразования может порушить часть оптимизаций.

Думаю, это можно обойти через std::launder (если я правильно понял механизм отмывания памяти) или разрешив только преобразование к указателям на константы.

pavelkryukov commented 2 years ago

если библиотека поддерживает итераторы, написание итератора-конвертера.

Хотел ответить, что можно написать только operator=(const T&) и operator T(). C одной стороны, будет явное и безопасное соответствие между полями; с другой -- итераторы и алгоритмы должны сгенерироваться и оптимизироваться.

struct Coords {
    int column;
    int row;

    void operator=(const XY& rhs) noexcept { column = rhs.x; row = rhs.y; }
    operator XY() const noexcept { return XY{column, row}; }
};

Но компилятор в таком копировании не хочет разглядеть memcopy/memmove, и пишет код самостоятельно. https://godbolt.org/z/TaWsxdc6o

Так что решения не вполне эквивалентны, но выглядит больше как недоработка компилятора.

topin89 commented 2 years ago

C одной стороны, будет явное и безопасное соответствие между полями; с другой -- итераторы и алгоритмы должны сгенерироваться и оптимизироваться.

Собственно, идея этого и ещё этого предложений в том, чтобы можно было обходиться без копирования массива, всего или поэлементно. И да, memcpy может оптимизироваться, а может и нет, и компилятор нам никогда не скажет, в чём проблема и заключена.

Это определённо оправдано, если тип, размер или выравнивание элементов структуры не совпадают. Но если это эффективно одно и то же, можно же дать как механизм как проверки, что struct{uint8_t r, g, b; }; ничем не отличается от std::array<uint8_t, 3>, так и возможность избирательно алиасить такие типы и, желательно, все производные.

pavelkryukov commented 2 years ago

Есть три способа передать массив: итераторы/ranges, С-указатель, ссылка/указатель на C++ класс. Если что-то упустил, поправьте пожалуйста.

Последний способ отбрасываем за общей немасштабируемостью (нельзя передать вектор вместо массива).

С-указателю нужно ваше приведение. Шаблонный итератор-конвертер без него тоже не строится (нельзя взять указатель/ссылку на временный объект), согласен.

Если предположить, что у нас есть рефлексия вроде #482 или #468, то мы должны перестать считать такой код UB:

template<typename To, typename From>
To* dealiasing_cast(From* ptr)
{
    static_assert(sizeof(To) == sizeof(From));
    static_assert(alignof(To) == alignof(From));
    static_assert(number_of_members<To> = number_of_members<From>);
    for ... (auto i = 0; i < number_of_members<To>; ++i)
        static_assert(std::is_same_v(type_of_member<To, i>, type_of_member<From, i>>);

    return reinterpret_cast<To*>(ptr);
}
AndreyG commented 2 years ago

P1912: Types with array-like object representations

topin89 commented 2 years ago

P1912: Types with array-like object representations

Круто! И думаю, можно слегка расширить, чтобы работала и на структурах типа таких:

struct PixOriented{
    int x;
    int y;
    double theta;
};

struct oriented_point{
    int x_offset;
    int y_offset;
    double theta;
};

Хотя шансов встретить такую структур существенно меньше, чем условные цвета и координаты, они есть. Уверен предложение можно расширить до условных:

union{
    struct T layoutas(int[2], float[1]){
        int x;
        int y;
        float z;
    };

    struct{
        int array_int[2];
        float array_float[1];
    };
}

А внутри считать всё массивом из таких вот анонимных структур

NN--- commented 2 years ago

Если структуры стандартно расположены то стандарт позволяет. Ещё можно посмотреть std::is_corresponding_member

https://www.reddit.com/r/cpp/comments/qs8ecs/is_there_a_way_to_reinterpret_cast_same_struct/