Как стать автором
Обновить

Комментарии 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)* в любой тип, и обращение по указателю далее будет валидным, если там существует объект.

Правило то такое действительно есть, только работает оно строго в другую сторону

memcpy работает только для тривиально копируемых и разрушаемых объектов. И нужно не угадывать, а гарантировать себе инвариантами нужный тип под void*.

Других реализаций высокоуровневого type erasure не существует

НЛО прилетело и опубликовало эту надпись здесь

Соглашусь, у меня формулировка подкачала. Но все-таки, я отвечал на комментарий где предлагается именно обращаться по указателю.

Что касается просто каста - в directx когда-то был официальный способ сеттить float через интерфейс, принимающий int - ровно reintepret_cast'ом из float в int.

С алайасингом и у меня коллиззии в памяти, поэтому я без толики иронии и предложил со мной поспорить :)

Речь же шла об исключении только для char* Чем тогда short* менее исключительный?

short* a = 0;
int* b = reinterpret_cast<int*>(a);

  1. short не гарантирует соответствие по длине байту для конкретной реализации и для конкретной машины

  2. Так исторически сложилось, это идиома языка, скорее всего это следствие повсеместного использования кодировки ASCII, длина символа в которой равна 8 битам. А байт это наименьшая адресуемая ячейка памяти (по крайней мере это верно для х86 и возможно для остальных архитектур времен создания языка C).

  3. Так написано в стандарте

Концепты тоже 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, неконстантный, может кидать исключения. И дальше мы бы могли наращивать энтропию, например сделать шаблонный возвращаемый тип.

А с концептами наоборот. Мы должны описать каждое из этих ограничений чуть ли не по отдельности, а для наращивания энтропии их можно снимать.

template
concept fooable = requires (T value) {
{ value.foo() } noexcept -> std::same_as<int>;
};

}

Не вижу отличий

Не вижу отличий

ну вы поставили ограничение noexcept (в моей версии функция может кидать исключения), и нет ограничения на то, что foo() неконстантный.

какая разница какое ограничение я поставил, смысл в том что вот, я задал нужные мне ограничения, в чем проблема?

я специально составил пример так, чтобы попытка реализовать его в виде концепта помогла вам понять, что именно я имею в виду.

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. А уж как вы его реализауете - это ваше дело. Сумеете без оверхеда - прекрасно.

Но в std::span нет никакого type erasure даже отдаленно. Об этом речь, а не об отсутствии оверхеда.

А не то такими темпами мы вообще все шаблонное программирование запишем в type erasure. Или пойдем дальше и фактически поставим знак эквивалентности между type erasure и любыми формами полиморфизма
Копайте глубже — любая компиляция в машинный код есть type erasure, был тип size_t или void*, а стал регистр rax :)

буквально любая функция, так как void* можно реинтерпретировать как что угодно

void* нельзя привести к указателю на функцию, как минимум потому что размер указателя на функцию может иметь другой размер. См https://godbolt.org/z/nhxjT9r5W

это метод, а не функция

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории