Comments 38
Хотя стоит отметить, что относительно массивов char нам приходится хранить размер, который мы могли бы знать на компиляции.
string_view могут быть constexpr, поэтому если объявить константу вида constexpr std::string_view something = "Test", то её прямое использование в моём понимании скорее всего размер соптимизирует.
В конструкторе из литерала внутри std::char_traits<T>::length а он и так constexpr.
Эти 2 "таски" делают разные вещи, под ними внутри генерируются разные типы "стейт машин", но для наблюдателя их тип одинаковый. А вот поведение при исполнении - разное.
и что что поведение разное? Типы же не стираются, вся типизация описана в task<T>
. Как минимум при использовании корутин мы нигде не обязаны восстанавливать T из возвращаемых корутиной значений, это само по себе является весьма однозначным критерием отсутствия стирания типов.
task в данном случае это нечто типа void* обернутого над стейт машиной внутри. Скорее всего task состоит из всего одного coroutine_handle<> который в свою очередь состоит из одного void*
Погодите, task
это тип возвращаемого значения, которое содержит coroutine_handle<promise>
, являющуюся по сути указателем на фрейм корутины (полностью сгенерированный компилятором), содержащий promise
. Разработчик нигде не обязан кастить coroutine_handle<promise>
в coroutine_handle<void>
или обратно, по крайней мере до тех пор, пока не захочет смешивать разнотипные корутины между собой.
в С все использовали чары для работы с сырыми байтами, поэтому для них в языке С++ исключение и указатель на них можно приводить к указателю на любой другой тип.
Можно это уточнить или привести примеры?
int main(int argc, char* argv[]) {
char c = 0;
char* a = &c;
int* b = a;
}
1.cc:4:8: error: cannot initialize a variable of type 'int *' with an lvalue of type 'char *'
int* b = a;
^ ~
1.c:4:12: warning: initialization of 'int *' from incompatible pointer type 'char *' [-Wincompatible-pointer-types]
4 | int* b = a;
| ^
В C++ вы можете делать так:
char* data = get_some_data();
std::cout << *reinterpret_cast<int*>(data) << std::endl;
Но не можете делать так:
float* data = get_some_data();
std::cout << *reinterpret_cast<int*>(data) << std::endl;
Ну или более наглядный пример
enum class MyByte : unsigned char{};
void foo_mybyte(MyByte* data) {
std::cout << *reinterpret_cast<int*>(data) << std::endl;
}
void foo_uchar(unsigned char* data) {
std::cout << *reinterpret_cast<int*>(data) << std::endl;
}
void foo_byte(std::byte* data) {
std::cout << *reinterpret_cast<int*>(data) << std::endl;
}
int value = 5;
foo_uchar(reinterpret_cast<unsigned char*>(&value)); // ok
foo_byte(reinterpret_cast<std::byte*>(&value)); //ok
foo_mybyte(reinterpret_cast<MyByte*>(&value)); // UB
Если объяснять простыми словами: Вы можете кастовать (char/unsigned char/std::byte)* в любой тип, и обращение по указателю далее будет валидным, если там существует объект. Но нельзя кастовать из произвольного типа в произвольный, и потом обращаться к этому указателю, даже если там существует такой объект
Все примеры одинаковое UB - кастить можно только к char* (или unsigned char/std::byte), а не наоборот. Обратно - только memcpy/std::copy/std::bit_cast.
Референс - https://eel.is/c++draft/basic.lval#11
Но не возражаю, если опровергнете.
UPD: перечитав примеры, согласен, что вторая часть вполне себе валидная, но пассаж про каст из char очень неоднозначный
если вы угадали с типом(динамическим) то можно. Если вы собираетесь флоат в инт переделать, то придётся memcpy
Обычно угадывать - такая себе идея, а главное зачем, если вышеупомянутые memcpy/std::copy/std::bit_cast с 99.9% вероятностью сделают то же самое (без оверхеда), но на 100% надежно?
Но тут речь даже о другом, цитируя комментарий выше:
Вы можете кастовать (char/unsigned char/std::byte)* в любой тип, и обращение по указателю далее будет валидным, если там существует объект.
Правило то такое действительно есть, только работает оно строго в другую сторону
Соглашусь, у меня формулировка подкачала. Но все-таки, я отвечал на комментарий где предлагается именно обращаться по указателю.
Что касается просто каста - в directx когда-то был официальный способ сеттить float через интерфейс, принимающий int - ровно reintepret_cast'ом из float в int.
С алайасингом и у меня коллиззии в памяти, поэтому я без толики иронии и предложил со мной поспорить :)
Речь же шла об исключении только для char*
Чем тогда short*
менее исключительный?
short* a = 0;
int* b = reinterpret_cast<int*>(a);
short не гарантирует соответствие по длине байту для конкретной реализации и для конкретной машины
Так исторически сложилось, это идиома языка, скорее всего это следствие повсеместного использования кодировки ASCII, длина символа в которой равна 8 битам. А байт это наименьшая адресуемая ячейка памяти (по крайней мере это верно для х86 и возможно для остальных архитектур времен создания языка C).
Так написано в стандарте
Концепты тоже type erasure. Вообще всё в C++ - type erasure. И ещё всё - монады.
Концепты не type erasure, это типы типов
Ну хорошо. Концепты - это meta type erasure. Часть информации о типе стирается, но результатом является не тип, а ограничение на тип.
Какой-то ментальный фристайл начался...
Концепт - это просто функция, принимающая тип и возвращающая булевое значение - соответствует ли тип набору требований. Плюс немного сахарка чтобы этим было удобно пользоваться. Говорить о них в категориях метатипов (в отличие от трейтов) мешает их "утиность" и ленивость. В качестве доказательства - функция, принимающая значение типа T
, ограниченного концептом C
, должна быть корректной относительно T
, а не C
.
Говорить о них в категориях метатипов (в отличие от трейтов) мешает их "утиность"
Почему мешает утиность, мне непонятно. С практической точки зрения это сильно увеличивает сферу применения концептов. Они могут работать с типами, которые ничего не знают о данном концепте.
и ленивость
Это не так. Например функция с обычными темплейтными параметрами может принять неопределённый в данной точке тип, главное, чтобы в месте инстанцирования тела функции тип был уже определён. Это невозможно в случае функции с концептифицированными параметрами, потому как неопределённый тип не может быть проверен на соответствие концепту.
В качестве доказательства - функция, принимающая значение типа
T
, ограниченного концептомC
, должна быть корректной относительноT
, а неC
Если вы имеете ввиду, что реализация функции корректна относительно Т, а не С - это так. Изначально Бьёрн хотел иметь более мощные концепты, ограничивающие не только интерфейс, но и реализацию, но потом отказался от этого из-за проблем с переиспользованием уже существующего кода. Попавшие в стандарт концепты являются развитием предложения Concepts Lite.
Концепт - это просто функция, принимающая тип и возвращающая булевое значение - соответствует ли тип набору требований.
Это не совсем так, ознакомьтесь с моим комментарием к другой статье https://habr.com/ru/post/645321/comments/#comment_23937457
Это невозможно в случае функции с концептифицированными параметрами, потому как неопределённый тип не может быть проверен на соответствие концепту.
я имел в виду ленивость относительно вычисления соответствия типа концепту, которое происходит только при сверке типа с концептом (в процессе поиска перегрузок или инстанцирования шаблонов), а не на этапе формирования типа.
Почему мешает утиность, мне непонятно. С практической точки зрения это сильно увеличивает сферу применения концептов.
Это не совсем так, ознакомьтесь с моим комментарием к другой статье
ну я же даже уточнил: "плюс немного сахарка чтобы этим было удобно пользоваться". Конечно же делать перегрузки по ограничениям через enable_if'ы намного сложнее, чем концептами. Да, сфера применения концептов шире чем у трейтов. Но это никак не меняет их природу, они всё еще остаются булевой функцией.
Разница с типом в том, что тип формируется путем описания его свойств, а концепты - путем наложения ограничений. В общем случае может быть довольно-таки сложно описать концепт так, чтобы из соответствия типа T
концепту C
гарантированно следовала корректность функции, написанной относительно C
и принимающей экземпляр T
а не на этапе формирования типа.
Я, правда, не понял, почему трейты с их неленивостью больше соответствуют высокому званию метатипа. Впрочем, возможно нечто отчасти похожее появится и в плюсах. Херб Саттер показывал пару лет назад в рамках предложения по интроспекции типов.
Конечно же делать перегрузки по ограничениям через enable_if'ы намного сложнее, чем концептами.
Если, например, задействована перегрузка функций с partial ordering аргументов, то условия в enable_if'ах будут расти факториально, что на практике исключает использование такого приёма при N > 3.
В общем случае может быть довольно-таки сложно описать концепт так, чтобы из соответствия типа
T
концептуC
гарантированно следовала корректность функции, написанной относительноC
и принимающей экземплярT
Я же вроде объяснил, почему, по крайней мере сейчас, от этого отказались. Скажу честно, на практике это не вызывает особых проблем.
Я же вроде объяснил, почему, по крайней мере сейчас, от этого отказались. Скажу честно, на практике это не вызывает особых проблем.
вы думаете я доказываю что концепты какие-то незавершенные или плохие, приводя ту же аргументацию, которую я приводил годы назад, и ту же позицию, которой я до сих пор придерживаюсь.
Но утверждаю я другое - что мыслить о концептах в категориях типов типов, метатипов или "meta type erasure" не совсем корректно. Потому что типы формируются иначе - путем добавления свойств, нежели ограничений. Это как пытаться заказать дизайн веб-сайта А. по наброску и Б. по ТЗ.
Концепты тоже формируются добавлениями свойств.
template<typename T>
concept A = B<T> && requires (T value) { value.foo(); };
Чем вам не свойства? Если бы концепты требовали полного описания всех свойств типа и любое лишнее было бы ошибкой, то концепту мог бы удовлетворять только один какой то тип))
формирование "добавлением свойств" бы выглядело как-то так (псевдосинтаксис):
metaclass Fooable {
public:
int foo();
}
И мы бы из этой строчки знали, что класс должен обладать методом foo(), который возвращает int, неконстантный, может кидать исключения. И дальше мы бы могли наращивать энтропию, например сделать шаблонный возвращаемый тип.
А с концептами наоборот. Мы должны описать каждое из этих ограничений чуть ли не по отдельности, а для наращивания энтропии их можно снимать.
std::bit_cast замещает многое упомянутое. Тема std::memcpy не раскрыта.
Что-то много воды понаписано для объема. Нет никакого "type erasure" в любых формах compile-time типизации. T *
- это не type erasure. std::span<T>
- это не type erasure. А не то такими темпами мы вообще все шаблонное программирование запишем в type erasure. Или пойдем дальше и фактически поставим знак эквивалентности между type erasure и любыми формами полиморфизма, включая compile-time полиморфизм. Зачем тогда придумывать новый термин, если термин "полиморфизм" уже есть?
Погодите ка, смотрите, классический пример - std::function, смысл в том что мы не знаем какой конкретно тип под ней на рантайме и можем его подменять, так?
Тогда объясните в чем разница с std::span< T >, который на рантайме может менять последовательность под ним с вектора на строку и вообще на любой contiguous набор T?
Про std::function
спору нет - это классический пример настоящего type erasure. std::function
- это фактически специализированный вариант std::any
, адаптированный специально под нужды функциональных объектов.
А std::span<T>
- это ни что иное как тонкий адаптор над обычным массивом. Очень тонкий. То есть это просто указатель T *
и размер. Все, с чем он может работать, должно тривиально превращаться в указатель и размер. Маловато для гордого звания type erasure.
а почему type erasure должно быть с оверхедом ? Где это правило написано?)
А не то такими темпами мы вообще все шаблонное программирование запишем в type erasure. Или пойдем дальше и фактически поставим знак эквивалентности между type erasure и любыми формами полиморфизмаКопайте глубже — любая компиляция в машинный код есть type erasure, был тип size_t или void*, а стал регистр rax :)
буквально любая функция, так как void* можно реинтерпретировать как что угодно
void* нельзя привести к указателю на функцию, как минимум потому что размер указателя на функцию может иметь другой размер. См https://godbolt.org/z/nhxjT9r5W
Обзор всего доступного в С++ type erasure