std::launder применим только если по "отмываемому" адресу действительно уже существует объект типа T. Об этом недвусмысленно написано в его описании по ссылке. Нельзя пытаться "отмывать" многомерный массив в одномерный, это не будет работать, потому что по соответствующему адресу находится многомерный массив, а не одномерный, и std::launder для этого не предназначен.
Как x2[1] может быть доступен через указатель на массив из 10 элементов?
Там написано не про это. Там написано, что x2[1] недоступно через &x2[0][0], а через приведенное сочетание reinterpret_cast и std::launder кто-то пытается притвориться, что доступно - ведь через int(*)[10] оно действительно должно быть доступно, но фактически в данном случае это не так. В некотором роде этот пример обратен вашей ситуации - тут одномерный массив (точнее, указатель на первый элемент одномерного массива) пытаются превратить в многомерный через std::launder.
Скорее, это намекает на то, что сначала допустили какую-то ошибку, а теперь лихорадочно пытаются костылить, чтобы эту ошибку обойти.
Ни std::launder, ни std::start_lifetime_as не являются "костылями" для обхода воображаемой "проблемы" с превращением многомерных массивов в одномерные (ну, или наоборот).
В частности, если они годятся для решения обсуждаемого вопроса, то не имеет значения, для чего они были задуманы изначально.
Могу сразу сказать, что вряд ли годятся :) По той же причине, по которой в C++ нельзя обращаться к "неактивным" элементам union- по одному адресу не могут одновременно "жить" (выражаясь казенным языком - быть within their lifetime) несколько объектов разных типов.
Нет. std::launder не предназначен для волшебного превращения одного типа по адресу в другой тип. Рекомендую ознакомиться с примерами в соответствующей статье на cppreference, там есть например такой:
int x2[2][10];
auto p2 = std::launder(reinterpret_cast<int(*)[10]>(&x2[0][0]));
// Undefined behavior: x2[1] would be reachable through the resulting pointer to x2[0]
// but is not reachable from the source
Однако, само название намекает, что данные "трюки" настолько востребованы, что для них даже добавили специальные функции.
А еще это как бы намекает что одних трюков с реинтерпрет кастами и лаундерингом недостаточно. Не говоря уж о том, что start_lifetime_as предназначены скорее немного для другого: например, есть у вас память, замапленная через mmap из файла какого-нибудь, и вы хотите превратить ее в объект (или массив объектов), чтобы потом с ней структурированно работать.
а потом, после включения C++2c, — потому что "cast from 'void *' is not allowed in a constant expression because the pointed object type 'int[2][4]' is not similar to the target type 'int[8]'".
Наверное, следует подождать, когда reinterpret_cast станет доступен в constexpr-функциях.
У reinterpret_cast есть правила насчет того, когда получившийся указатель может быть разыменован, а когда - нет (и эти правила эквивалентны правилам для указателя, получившегося в результате каста одного указателя в другой через void*, потому что это абсолютно эквивалентные операции). Вот это вот "is not allowed because the pointed object type is not similar to the target type" - это никак не изменится, даже если reinterpret_cast разрешат в constexpr. ИМХО все эти трюки просто-напросто опасны, не нужны и не стоят потраченного времени.
Ни один компилятор — не то, что не ругался на UB, но даже и не пикнул: все компиляторы сгенерировали правильный код, который отработал и напечатал ожидаемую последовательность.
Если подобные трюки делать в compile time, компилятор очень даже пикает.
Ни один компилятор — не то, что не ругался на UB, но даже и не пикнул: все компиляторы сгенерировали правильный код, который отработал и напечатал ожидаемую последовательность.
Это — результат правильного способа использования многомерного массива как одномерного.
Как-то я все-же не уверен насчет "правильного способа". Описание работы built-in subscript operator с cppreference:
The built-in subscript expression E1[E2] is exactly identical to the expression *(E1 + E2) except for its value category (see below) and evaluation order(since C++17): the pointer operand (which may be a result of array-to-pointer conversion, and which must point to an element of some array or one past the end) is adjusted to point to another element of the same array, following the rules of pointer arithmetic, and is then dereferenced.
When an expression J that has integral type is added to or subtracted from an expression P of pointer type, the result has the type of P.
[...]
Otherwise, if P points to the ith element of an array object x with n elements, given the value of J as j, P is added or subtracted as follows:
The expressions P + J and J + P
point to the i+jth element of x if i + j is in [0, n), and
are pointers past the end of the last element of x if i + j is n.
The expression P - J
points to the i-jth element of x if i - j is in [0, n), and
is a pointer past the end of the last element of x if i - j is n.
Other j values result in undefined behavior.
Проблема тут на мой взгляд в том, что в случае массива a[2][4] из статьи результатом выражения &a[0][0] является указатель, указывающий на первый элемент массива из четырех элементов (а не восьми), соответственно n из цитаты будет 4, а не 8. Даже если все компиляторы сейчас для вашего трюка генерируют правильный код, это отнюдь не означает, что так будет всегда. В общем, я бы такие сомнительные на мой взгляд преобразования все равно не делал.
Ну раз вы научились этим пользоваться, то вам, наверное, не составит труда привести практический пример.
Вам уже и без меня тут привели ряд практических примеров, но не в коня корм - вы продолжаете твердить, что слаще морковки ничего не бывает, а наколеночные скрипты, кодогенерирующие "конфиги" на целевом языке (а другим способом обрабатывать их в compile-time без задействования встроенных в язык средств compile-time выполнения не получится) - это очень удобно. Еще раз - говорите за себя.
goto и не в constant evaluated context-то могут не только лишь все. А в сабже goto вообще нет.
в этом бесполезном направлении.
Говорите за себя. Если вы так и не научились этим пользоваться, а вместо этого предпочитаете лепить наколеночные скрипты для кодогенерации и вызывать их через мейкфайлы - это еще не значит, что оно бесполезно.
Вот вам более интересный пример тогда (правда, на плюсах) - выражение вычисляется во время компиляции, как в примере "Мини-интерпретатор" из этой статьи, но берется оно при этом из внешнего текстового файла.
PyPl – это репозиторий программного обеспечения. Доступ его содержимому является штатной и фактически неотъемлемой частью Питона.
Вполне себе отъемлемой. Впрочем, не вижу смысла повторяться, @netch80 уже ниже все расписал.
то в vcpkg 2500 пакетов, а в PyPl – 600000. Это не просто количественное, а уже качественное различие.
Вот бы узнать, какой процент среди этих 600000 пакетов составляют разнообразные лефтпады. Впрочем, суть не в этом, а в том, что ваше утверждение, что "нейросети затащили в стандарт питона" просто-напросто ложно.
Ну да, как я и написал - пакетный менеджер. Использование PyPl в питончике для того, чтобы подключить какой-нибудь PyTorch - это нормально, а использование vcpkg в C++ для того чтобы подключить тот же libtorch - это ужас-ужас, я правильно понимаю?
Я имею в виду, что PyPl является первоисточником неотъемлемых частей реальной питоновской программы, выполняя такую же функцию, как системная библиотека C++.
Нет. "Такую же функцию, как системная библиотека C++" в питончике выполняет Python Standard Library (кстати, с кучей платформозависимых оговорок, см. раздел Notes on availability). А PyPl - это пакетный менеджер, его аналогами в C++ являются vcpkg, conan и тому подобное.
А вот и нет. Открываем TI-евский "Optimizing C/C++ Compiler User’s Guide" и читаем:
The compiler supports associative versions for some of the addition and multiply-and-accumulate intrinsics. These associative intrinsics are prefixed with “_a_”. The compiler is able to reorder arithmetic computations involving associative intrinsics, which may produce more efficient code.
[...]
However, this reordering may affect the value of the expression if saturation occurs at different points in the new ordering. [...] A rule of thumb is that if all your data have the same sign, you may safely use associative intrinsics.
То есть с одной стороны код будет лучше оптимизироваться компилятором, а с другой погромист должен сам думать, с какими данными эти ассоциативные интринсики можно применять, а с какими нет.
Ну скажем так на cppreference этот момент формулируется следующим образом:
When signed integer arithmetic operation overflows (the result does not fit in the result type), the behavior is undefined, — the possible manifestations of such an operation include:
it wraps around according to the rules of the representation (typically two's complement),
it traps — on some platforms or due to compiler options (e.g. -ftrapv in GCC and Clang),
it saturates to minimal or maximal value (on many DSPs),
it is completely optimized out by the compiler.
то есть, вообще говоря, знаковое переполнение (даже если оно завершается сатурацией) - это UB, а компилятор, как известно, имеет право выполнять любые оптимизации в расчете на то, что UB не происходит. К слову, у TI-евских DSP есть ассоциативные интринсики (например, _a_sadd - ассоциативное сложение с насыщением), так вот, их тоже нужно применять с осторожностью, т.к. компилятор их будет переупорядочивать при необходимости.
Да посмотрите на самые базовые возможности SDL в конце концов.
Ага, а еще посмотрите как эту SDL перехреначивают каждый мажорный релиз. Вот и в SDL3 по сравнению с SDL2 все перехреначили до неузнаваемости, там и ABI и API весь сломан - все попереименовали, типы поменяли (где раньше стол был яств теперь там гроб стоит был int, теперь bool)... И я уверен что еще будут фиксить всякие баги, которых там все еще миллион, со сломом API/ABI, а юзер будет вынужден лепить свои #if SDL_VERSION_ATLEAST( ... ) чтобы лавировать между всем этим с разной степенью успешности. В std такому точно не место. Идея запихать в std все на свете заведомо мертворожденная, никто этим заниматься не будет.
На любой платформе, использующей для представления целых чисел дополнительный код (а сколько-нибудь современные платформы все такие), самое большое положительное число переходит в самое большое отрицательное, и наоборот
Вполне себе современные DSP смотрят на вас с недоумением. Кстати, арифметика с насыщением весьма уязвима к потенциальному переупорядочиванию операций компилятором. Например, компилятор имеет право изменять порядок операций типа a + b + c, и разные компиляторы действительно выполняют это сложение в разном порядке. Вы, думаю, в курсе, но для остальных напомню, что оба компилятора используют стандартную для x86-64 calling convention, где для передачи параметров типа INTEGER используются регистры rdi, rsi, rdx и rcx по порядку, т.е. gcc производит сложение как ((a + b) + c) + d), а clang - как (a + b) + (c + d). Так вот, предположим в нашем гипотетическом примере с a + b + c, что a == INT_MAX, b == INT_MAX, а c == INT_MIN. Тогда, если сложение будет производиться компилятором в порядке (a + b) + c, то результатом будет -1, а если оно будет производиться в порядке a + (b + c), то результатом будет INT_MAX - 1.
std::launder
применим только если по "отмываемому" адресу действительно уже существует объект типаT
. Об этом недвусмысленно написано в его описании по ссылке. Нельзя пытаться "отмывать" многомерный массив в одномерный, это не будет работать, потому что по соответствующему адресу находится многомерный массив, а не одномерный, иstd::launder
для этого не предназначен.Там написано не про это. Там написано, что
x2[1]
недоступно через&x2[0][0]
, а через приведенное сочетаниеreinterpret_cast
иstd::launder
кто-то пытается притвориться, что доступно - ведь черезint
(*)[10]
оно действительно должно быть доступно, но фактически в данном случае это не так. В некотором роде этот пример обратен вашей ситуации - тут одномерный массив (точнее, указатель на первый элемент одномерного массива) пытаются превратить в многомерный черезstd::launder
.Ни
std::launder
, ниstd::start_lifetime_as
не являются "костылями" для обхода воображаемой "проблемы" с превращением многомерных массивов в одномерные (ну, или наоборот).Могу сразу сказать, что вряд ли годятся :) По той же причине, по которой в C++ нельзя обращаться к "неактивным" элементам
union
- по одному адресу не могут одновременно "жить" (выражаясь казенным языком - быть within their lifetime) несколько объектов разных типов.Нет.
std::launder
не предназначен для волшебного превращения одного типа по адресу в другой тип. Рекомендую ознакомиться с примерами в соответствующей статье на cppreference, там есть например такой:А еще это как бы намекает что одних трюков с реинтерпрет кастами и лаундерингом недостаточно. Не говоря уж о том, что
start_lifetime_as
предназначены скорее немного для другого: например, есть у вас память, замапленная черезmmap
из файла какого-нибудь, и вы хотите превратить ее в объект (или массив объектов), чтобы потом с ней структурированно работать.У
reinterpret_cast
есть правила насчет того, когда получившийся указатель может быть разыменован, а когда - нет (и эти правила эквивалентны правилам для указателя, получившегося в результате каста одного указателя в другой черезvoid*
, потому что это абсолютно эквивалентные операции). Вот это вот "is not allowed because the pointed object type is not similar to the target type" - это никак не изменится, даже еслиreinterpret_cast
разрешат вconstexpr
. ИМХО все эти трюки просто-напросто опасны, не нужны и не стоят потраченного времени.Если подобные трюки делать в compile time, компилятор очень даже пикает.
Как-то я все-же не уверен насчет "правильного способа". Описание работы built-in subscript operator с cppreference:
OK, смотрим Pointer arithmetic оттуда же:
Проблема тут на мой взгляд в том, что в случае массива
a[2][4]
из статьи результатом выражения&a[0][0]
является указатель, указывающий на первый элемент массива из четырех элементов (а не восьми), соответственноn
из цитаты будет 4, а не 8. Даже если все компиляторы сейчас для вашего трюка генерируют правильный код, это отнюдь не означает, что так будет всегда. В общем, я бы такие сомнительные на мой взгляд преобразования все равно не делал.Кстати, автор, а ты свои примеры сам-то пробовал запускать? Особенно парсеры?
Вам уже и без меня тут привели ряд практических примеров, но не в коня корм - вы продолжаете твердить, что слаще морковки ничего не бывает, а наколеночные скрипты, кодогенерирующие "конфиги" на целевом языке (а другим способом обрабатывать их в compile-time без задействования встроенных в язык средств compile-time выполнения не получится) - это очень удобно. Еще раз - говорите за себя.
goto
и не в constant evaluated context-то могут не только лишь все. А в сабжеgoto
вообще нет.Говорите за себя. Если вы так и не научились этим пользоваться, а вместо этого предпочитаете лепить наколеночные скрипты для кодогенерации и вызывать их через мейкфайлы - это еще не значит, что оно бесполезно.
Вот вам более интересный пример тогда (правда, на плюсах) - выражение вычисляется во время компиляции, как в примере "Мини-интерпретатор" из этой статьи, но берется оно при этом из внешнего текстового файла.
Даже не представляю, откуда вы это взяли.
В C++20 и выше забывать
&
тоже нельзя, даже если structured binding был объявлен какauto&
, абсолютно никакой разницы в этом плане нет.Глупости. Синтаксис просто чуть более громоздкий, но все будет работать.
Вполне себе отъемлемой. Впрочем, не вижу смысла повторяться, @netch80 уже ниже все расписал.
Вот бы узнать, какой процент среди этих 600000 пакетов составляют разнообразные лефтпады. Впрочем, суть не в этом, а в том, что ваше утверждение, что "нейросети затащили в стандарт питона" просто-напросто ложно.
Ну да, как я и написал - пакетный менеджер. Использование PyPl в питончике для того, чтобы подключить какой-нибудь PyTorch - это нормально, а использование vcpkg в C++ для того чтобы подключить тот же libtorch - это ужас-ужас, я правильно понимаю?
Нет. "Такую же функцию, как системная библиотека C++" в питончике выполняет Python Standard Library (кстати, с кучей платформозависимых оговорок, см. раздел Notes on availability). А PyPl - это пакетный менеджер, его аналогами в C++ являются vcpkg, conan и тому подобное.
А вот и нет. Открываем TI-евский "Optimizing C/C++ Compiler User’s Guide" и читаем:
То есть с одной стороны код будет лучше оптимизироваться компилятором, а с другой погромист должен сам думать, с какими данными эти ассоциативные интринсики можно применять, а с какими нет.
Ну скажем так на cppreference этот момент формулируется следующим образом:
то есть, вообще говоря, знаковое переполнение (даже если оно завершается сатурацией) - это UB, а компилятор, как известно, имеет право выполнять любые оптимизации в расчете на то, что UB не происходит. К слову, у TI-евских DSP есть ассоциативные интринсики (например,
_a_sadd
- ассоциативное сложение с насыщением), так вот, их тоже нужно применять с осторожностью, т.к. компилятор их будет переупорядочивать при необходимости.Ну это вы так думаете. Я бы настолько категорично на эту тему не высказывался, но я - это я, а вы - это вы.
Ага, а еще посмотрите как эту SDL перехреначивают каждый мажорный релиз. Вот и в SDL3 по сравнению с SDL2 все перехреначили до неузнаваемости, там и ABI и API весь сломан - все попереименовали, типы поменяли (где раньше
стол был яств теперь там гроб стоитбылint
, теперьbool
)... И я уверен что еще будут фиксить всякие баги, которых там все еще миллион, со сломом API/ABI, а юзер будет вынужден лепить свои#if SDL_VERSION_ATLEAST( ... )
чтобы лавировать между всем этим с разной степенью успешности. В std такому точно не место. Идея запихать в std все на свете заведомо мертворожденная, никто этим заниматься не будет.Вполне себе современные DSP смотрят на вас с недоумением. Кстати, арифметика с насыщением весьма уязвима к потенциальному переупорядочиванию операций компилятором. Например, компилятор имеет право изменять порядок операций типа
a + b + c
, и разные компиляторы действительно выполняют это сложение в разном порядке. Вы, думаю, в курсе, но для остальных напомню, что оба компилятора используют стандартную для x86-64 calling convention, где для передачи параметров типаINTEGER
используются регистрыrdi
,rsi
,rdx
иrcx
по порядку, т.е. gcc производит сложение как((a + b) + c) + d)
, а clang - как(a + b) + (c + d)
. Так вот, предположим в нашем гипотетическом примере сa + b + c
, чтоa == INT_MAX
,b == INT_MAX
, аc == INT_MIN
. Тогда, если сложение будет производиться компилятором в порядке(a + b) + c
, то результатом будет-1
, а если оно будет производиться в порядкеa + (b + c)
, то результатом будетINT_MAX - 1
.