Comments 154
Просто сишники не особенно хорошо читали теорию типов
А эта теория уже была тогда? Ну так то да сишный войд это юнит, а невозможность инстанцирования вероятно сильно упрощает язык типа сколько байт занимает значение? А массив? А если это мембер структуры? А нужен ему адрес? А зачем? Да и кому вообще нужен массив/мемберы юнитов/войдов? Можно конечно заморочиться и сделать специальные оптимизации везде где встречается подобная дичь, но это как раз и есть усложнение компилятора. Можно ничего не делать, но тогда драгоценные байтики памяти/регистры для аргументов/результата тех времён расходуются. Думаю поступили практично нет человека значения нет проблем.
Сделать sizeof(void) == 0 и всё.
Компилятор не пришлось бы усложнять, void-значение занимало бы 0 места в распределении регистров при передаче параметров или возврате результата.
Потом этот void проник в c# и мне в метапрограммировании приходится писать вдвое больше кода, отдельно для void-функций и обычных. Также, различать Action<T>
и Func<T,R>
Больше проблем, чем плюсов.
Сделать sizeof(void) == 0 и всё.
Сомневаюсь. Возникли бы примерно те же проблемы, по причине которых sizeof
пустой структуры в C++ составляет 1 байт а не 0.
верно, будут проблемы, размер массива структур не может быть нулевым. Плюс если я правильно помню, нулевой и отрицательные размеры зарезерированы под определенные нужды компилятора.
Ну в Rust как-то со всем этим справились, там могут быть и типы нулевого размера и unit type можно присваивать переменной.
Ну и как результат там плодятся "специальные случаи" - например, если засунуть unit type в Box а потом попытаться получить указатель не него - скажем через Box::as_ptr()
, то результатом будет невалидный указатель. Потенциальные грабли, заботливо разложенные для пейсателей unsafe кода.
Не вижу проблемы. Указатель типизированный, а значит мы знаем размер читаемой области. void* всегда может быть без проблем разыменован в пустое чтение, no-op. Невалидного void* указателя не существует. Вроде, никаких конфликтов с другими частями стандарта.
Не вижу проблемы
Эхехе. Ну вот взять например тот же вызов malloc для void (ведь раз его можно разместить "на стеке", значит, и на куче можно, так?). Что должен вернуть malloc? Реально выделить память нулевого размера он не может. NULL означает ошибку. Сделать "специальный случай" и не вызывать malloc вовсе (примерно так делает и растовский Box)? Сработает, если ты контролируешь реализацию, как растовский std контролирует реализацию своего Box (и то с проблемами, когда дело доходит до получения raw указателя), не сработает для пользовательского кода (например, шаблонного). Потребуются приседания, в том числе и со стороны пейсателя кода, по сути аналогичные приседаниям автора статьи - вставить constexpr if, придумать отдельную ветку для такого случая, как-то откуда-то сделать какой-то указатель и т.п. А в C вообще пришлось бы приседать в рантайме, а не в compile time.
Логично было бы вернуть nullptr
, но для malloc это значение зарезервировано под статус ошибки.
Пускай возвращает implementation-defined константу, для которой free ничего не делает. И всё, никаких приседаний со стороны писателя кода.
В стандартной библиотеке добавится проверка на size=0, но в malloc как правильно она уже есть (выбор пула аллокации в зависимости от размера блока). Во free уже есть проверка на nullptr, если константа будет (void*)-1
то изменение может быть бесплатным - вместо ptr==0
будет ptr<=0
Ну т.е. одного NULL (про который многие считают, что и его-то быть не должно) уже мало, нужно еще какое-то зарезервированное значение. Поддержки со стороны одного лишь компилятора оказалось маловато. Я просто уверен, что с дальнейшим продвижением в лес количество дров будет возрастать и дальше. А со структурами с минимальным размером в 1 байт ничего из этого просто не нужно, все работает "естественным образом".
Это вообще никак не влияет на стандарт и пользователей библиотек/компиляторов. Это детали реализации stdlib.
Если не хотите оптимизации, пусть malloc выделяет область в 0 байт, делает перед ней обычные заголовки области, и отдаёт уникальный адрес. На другие сценарии не повлияет. А если кто-то написал
std::vector<void>(30)
ну пусть аллокация будет. Раньше это вообще не компилировалось. А теперь такой странный код (который обычно и не встретится) будет не оптимальным.
Кроме malloc (по которому вы меня не убедили), других возражений нет?
Это вообще никак не влияет на стандарт и пользователей библиотек/компиляторов. Это детали реализации stdlib.
Да, и я о чем. К типам нулевого размера нужно "приучать" не только компилятор, но и libc, а это, как вы справедливо заметили, отдельная от конпелятора вещь.
Если не хотите оптимизации, пусть malloc выделяет область в 0 байт, делает перед ней обычные заголовки области, и отдаёт уникальный адрес.
Он и сейчас может это делать - создавать non-dereferenceable область "в ноль байт" (на самом деле нет) с уникальным адресом, которую потом нужно "освободить" через вызов free()
, а может вернуть NULL
. Это допустимое с точки зрения стандарта поведение.
Кроме malloc (по которому вы меня не убедили), других возражений нет?
Ну если брать например раст, то там, как я уже выше говорил, используется подход с non-dereferenceable указателем (со значением 0x1
) для объектов нулевого размера, расположенных "в куче" (по крайней мере Box возвращает такой вот "указатель" на содержащийся в себе unit type). Это весьма error-prone - скажем, в libc'шном memcpy()
такой "указатель" использовать нельзя вне зависимости от значения параметра count
. Но это сейчас, а какие соображения были против zero-sized void в свое время у создателей C, чем они руководствовались - я не знаю, но, видимо, какие-то основания у них были.
К типам нулевого размера нужно "приучать" не только компилятор, но и libc, а это, как вы справедливо заметили, отдельная от конпелятора вещь
Аргумент из серии "так сложилось". Я не предлагаю переделать текущий стандарт (это уже невозможно), я утверждаю, что если бы изначально void был полноценным значением с размером 0, это было бы намного лучше, чем есть сейчас. Естественно, тогда бы и libc писался под такие реалии.
Это весьма error-prone - скажем, в libc'шном memcpy() такой "указатель" использовать нельзя вне зависимости от значения параметра count
Если бы изначально в стандарте был 0-байт void, memcpy делал бы no-op при count=0. Да и сейчас, не думаю, что где-то реально для оптимизации используется ограничение, что при count=0, указатели должны быть валидными.
Он и сейчас может это делать - создавать non-dereferenceable область "в ноль байт" (на самом деле нет) с уникальным адресом, которую потом нужно "освободить" через вызов free(), а может вернуть NULL. Это допустимое с точки зрения стандарта поведение
То есть, это просто замечание, а не аргумент против.
То есть, это просто замечание, а не аргумент против.
Ну это не то чтобы аргумент против, просто я к тому, что в реальности размер все равно будет не 0, так зачем притворяться, что он 0. Вот реализация пустых структур в C++ и решила не притворяться.
Вы говорите, что malloc(0) потратит не 0? На что я могу возразить, что malloc(17) потратит не 17 байт, а больше.
Причём, есть реальная возможность чтобы malloc(0) тратил 0, но вы всеми лапами упираетесь против такой оптимизации libc, приплетая сюда какой-то "второй NULL" (который будет неизвестен никому, кроме free от libc, а значит, не потребует нигде ещё особого обращения).
Опять же, вы зацепились за malloc. В других же местах будет 0 байт, хоть структура, хоть массив из 100 void-ов.
Вы говорите, что malloc(0) потратит не 0? На что я могу возразить, что malloc(17) потратит не 17 байт, а больше.
И в чем тут "возражение"? Это взаимодополняющие утверждения, я бы сказал.
Причём, есть реальная возможность чтобы malloc(0) тратил 0, но вы всеми лапами упираетесь против такой оптимизации libc, приплетая сюда какой-то "второй NULL" (который будет неизвестен никому, кроме free от libc, а значит, не потребует нигде ещё особого обращения).
Ну это не то чтобы я упираюсь, а сами libc по большей части. Ведь malloc()
сам по себе не в курсе целевого типа, а только лишь необходимого размера, что ему мешало раньше применять такую "оптимизацию" при выделении блоков нулевого размера? Но как-то я в распространенных реализациях libc такой "оптимизации" не встречал. Видимо, на то есть причины.
Опять же, вы зацепились за malloc. В других же местах будет 0 байт, хоть структура, хоть массив из 100 void-ов.
Ну фактически тоже нет. Возьмем тот же раст, в котором "со всем этим справились" (нет). Уникальные адреса на стеке? Да. Значит, фактически размер уже не может быть "0 байт".
Уникальные адреса на стеке?
А что будет, если это требование не выполнить?
А что будет, если это требование не выполнить?
Пока не попробуешь - не узнаешь :)
P.S. Ну вообще в релизе по крайней мере в данном конкретном (я бы сказал, простейшем) случае оптимизатор это дело оптимизирует и размещает обе структуры по одному адресу. Но это именно что оптимизация, которая по сути своей опциональна и в отладочной сборке не выполняется. Т.е. никаких гарантий по факту раст на эту тему не предоставляет.
со структурами с минимальным размером в 1 байт ничего из этого просто не нужно, все работает "естественным образом"
За исключением того, что если нужно какое-то метапрограммирование, нужно отдельно описывать случай void-функции и не-void.
За исключением того, что если нужно какое-то метапрограммирование, нужно отдельно описывать случай void-функции и не-void.
Это касается только и непосредственно самого void
. Если на месте void
будет пустая структура, то из-за ее ненулевого размера никаких специальных приседаний с ней не требуется совершенно.
Ну так это примерно как получить указатель на константу или пустой слайс, это вообще ортогональные проблемы.
размер массива структур не может быть нулевым
Что случится, если разрешить нулевой размер структур?
зарезерированы под определенные нужды компилятора
Ввели ограничения для своего удобства, а потом оправдывают этим принципиальную невозможность
Ввели ограничения для своего удобства
Именно что для удобства, причём не только и не столько своего.
оправдывают этим принципиальную невозможность
А кто говорит о принципиальной невозможности? По-моему никто. Все говорят исключительно о цене реализации этих хотелок.
Пока ни одного примера, где рост цены неизбежен.
Предлагаемые вами выкрутасы со "вторым NULL" - это не рост цены?
Это не "второй NULL".
Там, где нужны проверки на NULL, они остаются без изменений и проверяется только обычный NULL. Проверка на заглушку нужна только на free, и она может быть бесплатной, как я показал раньше. Если не хочется отсекать половину адресного пространства (если у нас x86, а не x64), можно взять значение (void*)1
и тогда проверка на входе free
test ecx, -2
jz exit
вместо
test ecx, ecx
jz exit
Сделать sizeof(void) == 0 и всё.
Компилятор не пришлось бы усложнять, void-значение занимало бы 0 места в распределении регистров при передаче параметров или возврате результата.
Каждый элемент массива, в том числе, из void
'ов, должен иметь уникальный адрес.
С адресной арифметикой знакомы?
Можно дальше не объяснять?
Именно такова причина, по которой размер типа данных не может быть равен 0.
Ну так-то справедливости ради нужно сказать что адресную арифметику с void*
в стандартных C и C++ использовать нельзя, можно только в расширениях, в частности от GNU. В C void
- это, как там, "incomplete type that cannot be complete" или как-то так, поэтому его размер неизвестен и известен быть не может, какая уж тут адресная арифметика.
В C
void
- это, как там, "incomplete type that cannot be complete" или как-то так, поэтому его размер неизвестен и известен быть не может, какая уж тут адресная арифметика.
Верно, но речь-то о том, что (я отвечал на эту цитату):
Сделать sizeof(void) == 0 и всё.
После этого препятствий для самой адресной арифметики нет.
Каждый элемент массива, в том числе, из void'ов, должен иметь уникальный адрес.
Сможете объяснить, почему?
С адресной арифметикой знакомы?
Да
Можно дальше не объяснять?
Нужно объяснять. Потому что все считают это очевидным, а как сформулировать словами, так впадают в ступор.
Сможете объяснить, почему?
Для различения объектов при доступе к ним через указатели.
Вообще, первая же найденная мной ссылка по данному вопросу описывает несколько вариантов, в которых возникли бы проблемы, если бы тип был нулевого размера.
Нужно объяснять. Потому что все считают это очевидным, а как сформулировать словами, так впадают в ступор.
В ступор впадают не все.
Адрес следующего элемента в массиве отличается от адреса текущего на размер элемента. Если размер элемента равен 0, то адреса всех элементов в массиве будут иметь один и тот же адрес, и их невозможно будет различить по адресам.
Вообще, первая же найденная мной ссылка по данному вопросу описывает несколько вариантов, в которых возникли бы проблемы, если бы тип был нулевого размера.
Ну, этими аргументами вы не проймете :) В расте, например, такой код будет работать по-разному будучи собранным в дебаге и релизе, но это никого из апологетов раста не смущает, все привыкли, дескать, "а чего вы хотите от объектов с нулевым размером", как в анекдоте про доктора, который отвечает пациенту "а вы так не делайте". И, в принципе, можно сказать, что они в своем праве, для них это нечто вроде вкусовщины.
Ну, этими аргументами вы не проймете :)
Абсолютных аргументов здесь и нет, это — вопрос выбора.
В расте, например, такой код будет работать по-разному будучи собранным в дебаге и релизе, но это никого из апологетов раста не смущает, все привыкли, дескать, "а чего вы хотите от объектов с нулевым размером", как в анекдоте про доктора, который отвечает пациенту "а вы так не делайте".
Что характеризует выбор, сделанный в Rust'е.
В расте, например, такой код
Однако, так — нечестно, так и в C/C++ адреса разные будут.
Вот так — куда честнее.
Первый раз писал на Rust'е, и больше не хочу. Компилятор мне ещё указывает, в каком регистре идентификаторы заводить.
И, в принципе, можно сказать, что они в своем праве, для них это нечто вроде вкусовщины.
Отлично, ребята сделали себе игрушку, но почему им теперь обязательно нужно "осчастливливать" таким же выбором C/C++?
Апологеты C/C++, почему-то не пытаются осчастливливать Rust.
Для различения объектов при доступе к ним через указатели
void - не объект.
Вообще, первая же найденная мной ссылка
Не вижу проблем, чтобы все экземпляры void были isSame.
Очевидно, если программист захочет различать экземпляры, он не будет заводить их с типом void.
Адрес следующего элемента в массиве отличается от адреса текущего на размер элемента. Если размер элемента равен 0, то адреса всех элементов в массиве будут иметь один и тот же адрес, и их невозможно будет различить по адресам
И следствие какое из этого? Пример какой-нибудь, поближе к практике...
void - не объект.
А что это тогда?
Не вижу проблем, чтобы все экземпляры void были isSame.
Они в принципе не могут всегда быть одним экземпляром, пример я приводил здесь.
И следствие какое из этого? Пример какой-нибудь, поближе к практике...
Следствием является то, что адреса всех элементов в массиве становятся равны и поэтому не различимы по адресу.
По той ссылке, которую я раньше приводил, есть ещё примеры проблем, возникающих из-за одинаковости адресов различных объектов.
А что это тогда?
В си различаются простые типы (int, char) и составные (struct, class). Объекты экземпляры сложных типов. В-общем, не принципиально, вопрос определений.
Они в принципе не могут всегда быть одним экземпляром, пример я приводил
Я написал не точно, вы дали бесполезный (для меня) ответ.
Если уж совсем строго, моя реплика должны была быть такой:
Не вижу проблем, на разных экземплярах void, фунция isSame, представленная по ссылке вернула true, если экземпляры расположены последовательно. Но вот не вижу ни одного практического случая, где их нужно было бы отличать по адресу.
По той ссылке, которую я раньше приводил, есть ещё примеры проблем
Точно есть? И вы согласны с тем, что это реальные проблемы?
EmptyClass o1;
EmptyClass o2;
EmptyClass * po = &o;
po->foo();
Should the foo method be called on o1 or o2?
У void нет методов - нет проблем.
Если представить объект, состоящий из void-ов, то у объекта нет состояния, значит и поведение всех экземпляров одинаковое. Если это разные типы, а вызов виртуальный, значит есть vptr и объект уже не нулевого размера.
what will be deleted if they both have the same address?
Про malloc/free выше 100500 сообщений было. Нет там никаких проблем.
_countof для массива из void не скомпилируется из-за деления на 0. Подумаешь, пользовательский макрос не компилируется. Тут нужны примеры, где это могло встретиться в реальном коде. Размер массива знаем при его объявлении. Не хочется дублировать выражение - можно сделать constexpr.
В си различаются простые типы (int, char) и составные (struct, class).
Статья — по C++.
У void нет методов - нет проблем.
Вы — в курсе, что методы — это синтаксический сахар, и на самом деле вызывается функция, которой первым параметром передаётся ссылка на объект?
Если представить объект, состоящий из void-ов, то у объекта нет состояния, значит и поведение всех экземпляров одинаковое.
У объектов пустого класса тоже нет состояния, и — что?
Тут нужны примеры, где это могло встретиться в реальном коде.
Для примеров необходимо отдельно исследовать вопрос, чтобы найти что-то менее тривиальное.
Пока всё довольно бесполезное и потенциально опасное, например, станут возможны ссылки на void
, поскольку теперь можно будет разыменовать указатель на void
, но эти к этим ссылкам нельзя будет обращаться.
методы — это синтаксический сахар, и на самом деле вызывается функция, которой первым параметром передаётся ссылка на объект?
В плюсы пока не завезли методы для простых типов, так что с этой стороны они пока ещё не объекты. Или можно писать (42).to_string()
, я что-то пропустил?
У объектов пустого класса тоже нет состояния, и — что?
Для пустых объектов без разницы, на каком экземпляре вызывать метод, метод не может работать с данными объекта, поэтому совпадение адресов не ломает ничего.
Пока всё довольно бесполезное и потенциально опасное, например, станут возможны ссылки на void, поскольку теперь можно будет разыменовать указатель на void, но эти к этим ссылкам нельзя будет обращаться
В чём опасность, если обращение к void - это no-op?
В плюсы пока не завезли методы для простых типов, так что с этой стороны они пока ещё не объекты. Или можно писать
(42).to_string()
, я что-то пропустил?
Я о другом, — в обратную сторону.
Что это за this
такой в методах, откуда он там берётся?
Для пустых объектов без разницы, на каком экземпляре вызывать метод, метод не может работать с данными объекта, поэтому совпадение адресов не ломает ничего.
А если метод возвращает адрес объекта?
Объект характеризуется не только данными, но и адресом данных.
Вот если бы ещё и с адресами нельзя было работать...
В чём опасность, если обращение к void - это no-op?
Если no-op, тогда — другое дело.
Но это не отменяет проблем с неразличимостью адресов.
Объект характеризуется не только данными, но и адресом данных.
а какой адрес у регистра ecx ?
Что это за this такой в методах, откуда он там берётся?
Я потерял нить диалога. Сначала вы доказываете, что void - объект
void - не объект
А что это тогда?
А теперь уже и this упоминаете. Какой this в операциях с простыми типами?
Объект характеризуется не только данными, но и адресом данных.
int - объект? Откуда у rvalue адрес?
Вот если бы ещё и с адресами нельзя было работать
Работайте, но только для void уникальность не будет гарантирована.
Но это не отменяет проблем с неразличимостью адресов
Пришли к началу. Спрашиваю "какие проблемы, конкретно, на практике", отвечают "нууу... проблемы".
Я потерял нить диалога. Сначала вы доказываете, что void - объект
Не в том смысле, что это — класс, а в смысле -- некий intance.
А теперь уже и this упоминаете. Какой this в операциях с простыми типами?
Нет, речь о том, что вызов метода — синтаксический сахар.
#include <cstdlib>
#include <functional>
#include <iostream>
void f(int n, int m) {
std::cout << __func__ << ", n: " << n << ", m: " << m << std::endl;
}
struct S {
void m(int n) {
std::cout << __func__ << ", this: " << this << ", n: " << n << std::endl;
}
} s;
int main() {
std::invoke(f, 1, 2);
std::invoke(&S::m, s, 3);
return EXIT_SUCCESS;
}
Видите, в обоих случаях у std::invoke
— по 3 параметра.
Но у метода здесь — один параметр, а у функции — два.
Потому что у метода, на самом деле, — тоже два параметра в силу того, что у него есть ещё неявный параметр this
, несмотря на то, что он отсутствует в синтаксисе как параметр.
Поэтому нет принципиальной разницы между вызовом метода у объекта класса и вызовом функции, которой передан явный адрес/ссылка на некий instance.
Я на это отвечал:
У void нет методов - нет проблем.
Если же говорить в общем случае, у объекта нулевого размера можно передавать в this что угодно, обращение по этому указателю - no-op.
Если же говорить в общем случае, у объекта нулевого размера можно передавать в this что угодно, обращение по этому указателю - no-op.
По данным — согласен.
Но у instance'ов ещё и адреса есть, их не только по данным, но и по адресам можно различать.
Поэтому передавать в this
что угодно — нельзя.
Непустые объекты прекрасно отличаются.
Различать пустые объекты, зачем? Если хочется иметь уникальный идентификатор, и для этого выполняется аллокация пустого объекта, то см. выше коммент с fetch_add(1)
Различать пустые объекты, зачем?
Уже обсуждали пробег по массиву указателями в уже написанном шаблонном коде.
Это не ответ на вопрос "зачем".
Это мелкие грабли реализации алгоритмов, которые можно несложно обойти - итераторы можно специализировать, а range-based for по массиву компилятор может сделать корректно.
Это не ответ на вопрос "зачем".
Проблема останется, даже если незачем.
Это мелкие грабли реализации алгоритмов, которые можно несложно обойти - итераторы можно специализировать,
А если вместо итераторов в коде явно берётся адрес через std::addressof
?
А что мы теряем? Сейчас невозможно было создать контейнер с элементами нулевого размера. С новой фичей можно, но нужно быть осторожнее с итерациями. То есть, появляется новая возможность, старое ничего не ломается.
А что мы теряем? Сейчас невозможно было создать контейнер с элементами нулевого размера. С новой фичей можно, но нужно быть осторожнее с итерациями. То есть, появляется новая возможность, старое ничего не ломается.
6 часов назад вы согласились на ненулевой размер.
Согласились именно потому, что старое ломается.
Указатели — не итераторы, хотя и могут быть использованы, как таковые.
6 часов назад вы согласились на ненулевой размер.
Потому что мне важен void как полноценный тип, а его размер не так важен. Было бы логично иметь 0, но если не хочется делать исключения для итераций по адресам, пусть будет 1. Хотя, решение с 1 мне кажется некрасивым.
Согласились именно потому, что старое ломается.
Не поэтому, а потому что понял, что спорить дальше бесполезно. Старого (void как полноценный тип) не существует, нечему ломаться.
Это просто следствие того, что потребовали, что каждый элемент массива должен иметь свой адрес. Чем это требование обосновано? Видимо, пока его писали, просто забыли, что элемент может иметь нулевой размер, автоматических решалок-то ограничений еще не было, никто противоречивость и не заметил
Это просто следствие того, что потребовали, что каждый элемент массива должен иметь свой адрес. Чем это требование обосновано?
Как минимум тем, что иначе, как я написал выше, будет невозможно различать различные объекты по адресам.
Видимо, пока его писали, просто забыли, что элемент может иметь нулевой размер, автоматических решалок-то ограничений еще не было, никто противоречивость и не заметил
Сомневаюсь, что забыли и не заметили.
Если вернуться к основам, то тип характеризуется набором операций, определённых над объектами этого типа, а если это ещё и тип данных, то — и размером.
С размером — всё понятно, он равен 0.
Какие операции были бы уместны для объектов такого типа?
Так объект нулевого размера всегда один, что там различать?
Операции -- ну, вот в том же Rust какие-то операции нашлись?
Так объект нулевого размера всегда один, что там различать?
Это он в одном и том же массиве — "один".
А, например, в такой структуре:
struct {
void v0;
char c;
void v1;
} s;
Адреса &s.v0
и &s.v1
, очевидно, будут различными, и это будут разные объекты.
Операции -- ну, вот в том же Rust какие-то операции нашлись?
Rust — это Rust, а здесь — C++.
Так какие операции уместны для типа с нулевым размером, кроме взятия адреса и операции "запятая" (которая, кстати, и сейчас работает для выражений типа void
)?
Какова "польза" от наличия такого типа?
Перевешивает ли она вред от потери возможности различать объекты по адресам?
А адреса &s
и &s.v0
будут одинаковыми, но снова очевидно, что это разные объекты. И что?
Для начала надо бы понять, зачем нам вообще различать объекты по адресам. Что это дает? Если сами объекты неразличимы, то зачем требовать, чтобы их адреса различались?
Второе, зачем нам требовать, чтобы объекты нулевого размера вели себя всегда точно также, как объекты ненулевого. Ноль -- уже само по себе особое число, почему требование равнять его на других? В математике вы же не требуете, например, чтобы умножение 0 на 1 давало 1, хотя все остальные числа при умножении на 1 дают 1. Почему с размерами типов должно быть по другому?
А адреса
&s
и&s.v0
будут одинаковыми, но снова очевидно, что это разные объекты.
Адреса массива и его первого элемента тоже совпадают.
s.v0
— подобъект объекта s
, здесь нет ничего удивительного.
Добавьте ещё оно поле ненулевого размера перед полем v0
, и тогда адреса s
и s.v0
перестанут совпадать.
К обсуждаемому это не относится.
И что?
Речь шла о том, один ли объект нулевого размера, или их может быть несколько.
Вы утверждали, что — один, я показываю, что их может быть несколько.
Для начала надо бы понять, зачем нам вообще различать объекты по адресам. Что это дает?
Например, можно защититься от присваивания самому себе.
Если этого — мало, можно почитать эту ветку на SO.
Если сами объекты неразличимы, то зачем требовать, чтобы их адреса различались?
Для начала ответьте на вопросы:
Какие операции уместны для типа с нулевым размером, кроме взятия адреса и операции "запятая"?
и
Какова "польза" от наличия такого типа?
А то вы обсуждаете, как должен быть введён этот тип, забыв обдумать, а — нужен ли он такой вообще?
Мы обсуждаем
void
, там, во-первых, это присваивание мы не пишем руками, а, во-вторых, у него нет рантайм-поведения, потому что типvoid
пустой, поэтому и защищаться от него не надо: всё равно скомпилируется вnop
.
Функции бывают шаблонными.
Ни одна из этих причин не применима к
void
просто потому, что все объекты этого типа одинаковые по определению, и различать их необходимости нет.
Хорошо, шаблонная функция в давно написанном коде, которая выполняет итерации надо массивом с помощью явных указателей, а не итераторов, будет работать неправильно для типа void
с нулевым размером.
Упрощает метапрограммирование. Можете, например, перечитать пост, под которым мы тут комментарии пишем.
Но вносит неустранимые проблемы, указанные мной выше.
Покажете шаблонную функцию, где нужно защищаться от присваивания самому себе, особенно в случае с
void
?
Не смешно.
Нет, она вообще не будет работать (как и сегодня), потому что нулевой размер
void
не означает, что надо автоматически разрешить адресную арифметику сvoid*
. Но, впрочем…
Её нет необходимости "разрешать", она и так "разрешена" принципиально для любого типа, для которого в текущей точке программы известен его размер.
… даже если бы адресная арифметика с
void*
была разрешена в предположенииa[i] = a[0]
∀i, то это в частности бы означало, что указатель на начало массива совпадал бы с указателем на его конец
Что означает отсутствие элементов.
Типичное условие окончания цикла на итераторах видели?
отработал бы ровно ноль раз, потому что
pos = start = end
.
А должен отработать столько раз, сколько элементов в массиве.
Что, кстати, более чем согласно с теорией, потому что элементы массива
void
ов неразличимы, поэтому можно делать ноль итераций: никакой новой информации от какого бы то ни было элемента вы не узнаете.
А если в теле цикла вызов функции с побочным эффектом?
Вот здесь, благодаря наличию подходящих расширений в компиляторах, я показываю, что один цикл, работающий через адресную арифметику, не отработает ни для одного элемента, а другой, по индексам массива, отработает, как положено, для каждого элемента.
Очевидно, что такое введение "обновлённого" типа void
в язык никуда не годится.
Как красиво, когда теория достаточно консистентна, чтобы приводить ко вполне ожидаемым результатам даже в, казалось бы, весьма крайних случаях, не правда ли?
В первую очередь важна правильность, а не красота.
Если один вид цикла по массиву приводит к итерациям по всем элементам, а другой — "делает вид", что элементов нет, то зачем нужен такой void
?
Не уверен, что согласен с вами в видении проблем.
Кто ж вам запретит не соглашаться?
Она де-факто запрещена сейчас для
void*
, почему — неважно.
Никакого запрета нет.
Очень важно, почему.
Адресная арифметика не работает не только с void
:
struct s;
void fun0(s *p, size_t const n) {
for (s const *const end{p + n}; p < end; ++p) {
*p = {};
}
}
struct s {
};
void fun1(s *p, size_t const n) {
for (s const *const end{p + n}; p < end; ++p) {
*p = {};
}
}
Сообщение об ошибке, но исключительно для функции fun0
:
source>:7:28: error: arithmetic on a pointer to an incomplete type 's' 7 | for (s const *const end{p + n}; p < end; ++p) {
Как только размер типа становится известен, так сразу начинает работать адресная арифметика, причём, вполне определённым образом, поэтому функция fun1
уже может использовать адресную арифметику с типом s
.
Но тип-то — тот же самый для обеих функций, их тела — идентичны!
Никаких запретов на адресную арифметику нет, но есть условие, при котором она становится возможна.
Вы не хотите этого замечать, поэтому пытаетесь утверждать, что причина, по которой адресная арифметика не работает для void *
, не важна.
Да,
it != end
, примерно как я и пишу ниже.
Очевидно, что такая проверка не отличает отсутствие элементов от их наличия в массиве объектов с нулевым размером, и это — проблема.
Значит, вы используете ваш массив как счётчик.
Во-первых, это -- далеко не единственный вариант, а, во-вторых, — имею право.
Зачем вы это делаете? Просто передавайте число.
Никто не может указывать программисту, что и как делать в пределах Стандарта. Поэтому вопрос "зачем" — бессмысленный, а указания, как и что программисту делать — неуместны.
При этом текущий
void
тоже «никуда не годится», потому что метапрограммировать сложно
Это — куда меньшая проблема, и она — одна.
А вот чтобы наткнуться на упомянутые вами проблемы, нужно зачем-то сделать массив
void
ов и потом на его элементах ещё вызывать функции с сайд-эффектами, чтобы вообще заметить, что там какая-то ерунда с количеством элементов.
Массив не void
'ов, а элементов типа T
, который может выводиться и как void
.
Ну и ещё нужно в очередной раз обделаться с дополнением стандарта языка (чтобы зачем-то разрешить адресную арифметику на
void
ах, например), но тут сомнений в способностях Комитета у меня нет.
Она нигде и не запрещалась, но условием её работы является наличие возможности получить размер элемента.
При этом есть такие типы, которые — complete, но адресная арифметика для них все равно невозможна, потому что операция sizeof
для этих типов не определена.
Опять же, суть здесь — в наличии возможности знать размер элемента, а не в чём-то другом, типа, complete тип или incomplete и так далее, а не в каких-то выдуманных вами запретах.
в плюсах (и в C) невозможно сделать полностью правильный и консистентный язык просто потому, что там уже заложено достаточно противоречий. Поэтому вопрос сводится к тому, с какими из них вы готовы мириться. Мне метапрограммирование представляется более важным, чем особенности итерации по массивам из
void
ов (которые сейчас вообще запрещены, опять же).
Похоже, у вас много достаточно безапелляционных хотелок, и это очень искажает ваше восприятие вплоть до "запрета" адресной арифметики для void *
, которого нет.
Действительно, ведь у
void
нет других применений, кроме как создавать из него массивы и потом по ним итерироваться.
Код бывает шаблонный, а тип может быть выведен как void
.
Но даже если и явный void
в качестве элементов массива — вы не можете запретить даже это после того, как станет возможным определить размер типа void
.
Весь разговор о метапрограммировании, весь исходный пост — он про что-то другое.
Нет, сейчас разговор не о метапрограммировании, а — о том, что такой способ сделать тип void
complete, при котором его размер равен 0, порождает такие проблемы, с которыми смириться нельзя.
Кстати, я бегло перечитал тред, но не заметил ответа на другой вопрос: чем вам не нравится
sizeof void = 1
?
Очевидно, что такого ответа, чем не нравится, нет.
В качестве такого типа может выступать пустая структура, но человек, согласившийся на размер void
'а, равный 1, сказал, что ему нужно, чтобы именно тип void
обладал свойствами пустой структуры, заведение своей структуры его не устраивает.
суть здесь — в наличии возможности знать размер элемента, а не в чём-то другом
Но даже если и явный void в качестве элементов массива — вы не можете запретить даже это после того, как станет возможным определить размер типа void
Возможный компромис - зафиксировать sizeof(void)==0, но запретить прибавление скаляра к указателю на тип нулевого размера, как следствие, запретить брать разность таких указателей. Тогда шаблонный код, использующий end-указатель на конец контейнера, как критерий остановки итерации, просто не скомпилируется.
человек, согласившийся на размер void'а, равный 1, сказал, что ему нужно, чтобы именно тип void обладал свойствами пустой структуры
Если речь обо мне, то вы неверно поняли. Мне нужно, чтобы void был инстанциируемым типом. Размер 0 - просто пожелание, гармонирующее с моим понимаем красоты, но не обязательное требование.
заведение своей структуры его не устраивает
Да, потому что много кода написано с void, а не пустой структурой.
Возможный компромис - зафиксировать sizeof(void)==0, но запретить прибавление скаляра к указателю на тип нулевого размера, как следствие, запретить брать разность таких указателей.
То есть, явно запретить адресную арифметику для таких типов, нарушив стройную модель: операция sizeof
определена для типа — есть адресная арифметика, не определена — нет.
Шансов у такого предложения будет очень мало.
Если речь обо мне, то вы неверно поняли. Мне нужно, чтобы void был инстанциируемым типом. Размер 0 - просто пожелание, гармонирующее с моим понимаем красоты, но не обязательное требование.
Да, о вас, но понял я правильно.
Раз — пожелание, то вы и согласились на размер 1.
А это снимает главную проблему с адресной арифметикой.
Шансов на void
, который будет complete-типом с размером 1, невообразимо больше, чем на тот же void
с размером 0.
Но я боюсь, что уже имеется очень много шаблонного кода, который опирается на то, что void
— incomplete-тип (не инстанцируемый), и этот код сломается, если void
сделать complete-типом, поэтому шансов даже у такого варианта — нет.
То есть, явно запретить адресную арифметику для таких типов, нарушив стройную модель
Стройная модель не нарушается: если хотим, чтобы итерирование по адресам не приводило к непонятным багам, значит запрещаем увеличивать указатель на 0 при итерациях.
боюсь, что уже имеется очень много шаблонного кода, который опирается на то, что void — incomplete-тип (не инстанцируемый), и этот код сломается
Сходу не могу придумать пример. Подскажете?
Ох лол, я сконструировал такой пример, но там gcc и clang показывают разные результаты, что в очередной раз намекает нам, что плюсы безнадёжно сломаны
Нет, сломаны компиляторы, а не плюсы.
Если вы поотматываете версии компиляторов в прошлое, то увидите, что проблема была и у gcc, и у MSVC.
Забавно, что gcc вполне себе жрёт
typename = decltype(void{})
,
А что, разве не должен?
и то, что всех это на практике устраивает, как бы намекает нам, что всем пофиг.
Нет, отмотка версий компиляторов позволяет установить, что проблему постепенно исправляют.
Случай — очень редкий, видимо, поэтому процесс идёт медленно.
На данный момент проблема осталась только у clang'а.
Наверное, нет, потому что
void
— неинстанциируемый тип, и поэтому это должно быть невалидно даже вdecltype
-контексте.
А вы разве его инстанцируете?
Но я не уверен, конечно
Вот это уже — больше похоже на правду.
Вы только что утверждали, что сломается очень много шаблонного кода, который опирается на неполноту
void
.
Да, сломается.
Это в том числе такой код
Нет, не такой.
(и кроме (не)возможности инстанциировать неполный тип с очевидными оговорками про параметры конструктора и невозможности применить
sizeof
к самомуvoid
я не могу придумать вообще ничего).
Ничего особо мудрёного придумывать и не требуется.
Требуется грамотно реализовать.
Теперь вы пишете, что это очень редкий случай.
Да, редкий.
Вам не кажется, что в таком варианте вашего кода clang вдруг почему-то резко "починился" и стал работать правильно?
Правильно в данном случае это — так, как я говорю, а не как вы ожидаете.
Или скажете, что теперь и clang сломан, раз вы ожидаете от него другого поведения?
Можно как-то определиться, если не сложно?
Это не мне следует определиться, а вам понимание своё на новый уровень поднять.
Вы неправильно реализовали свой пример, у вас там нет инстанцирования.
Чтобы оно было, его следует доработать, например, так.
Видите, теперь и gcc работает так, как вы задумывали.
Или, если максимально близко к вашему варианту, а также используя невозможность ссылок на void
, так.
И опять gcc "заработал правильно".
Какая проблема? Он как раз правильно работает в данном случае.
Вы не поняли сути происходящего, не поняли, почему clang в данном случае так работает.
Я вам предлагал увидеть такую же работу и других компиляторов, путём отматывания их версий на более древние.
То есть, в других компиляторах эту ошибку со временем исправили, неисправленным остался только clang.
Поэтому, процитировав ещё раз:
Ох лол, я сконструировал такой пример, но там gcc и clang показывают разные результаты, что в очередной раз намекает нам, что плюсы безнадёжно сломаны
повторю: сломаны были компиляторы, а не плюсы.
Сейчас почти все компиляторы починены, кроме clang'а.
А вот что в этой ситуации "лол", так это — отнюдь, не компиляторы и, тем более, не C++.
Да.
void{}
что делает, по-вашему?
Создаёт prvalue.
Но я и в этом не вижу глубокой проблемы, потому что по факту подобные изменения никого не останавливали: в C++20 завезли (P0960) инициализирование агрегатов из скобочек, и те шаблоны, которые раньше опирались на не-well-formedness выражений
T(a, b, c)
теперь будут работать неправильно для некоторыхT
. И ничего, людей это не смущает.
Да, нехорошо получилось.
Однако, это касается довольно узкого круга случаев, к тому же, появляется некая унификация.
В случае же с void
круг случаев значительно шире.
Нет, не кажется. Что такое
void()
и чем это отличается отvoid{}
?
Значит, самостоятельно вы не смогли разобраться?
Пока что вы показываете полное незнание языка, если для вас
void()
иvoid{}
одно и то же, и если вы считаете, чтоvoid()
что-то там инициализирует, аvoid{}
— нет.
Во втором пункте этой части Стандарта написано:
Otherwise, if the type is cv void and the initializer is () or {} (after pack expansion, if any), the expression is a prvalue of the specified type that performs no initialization.
Так одно ли и то же void()
и void{}
теперь, после приведения мной цитаты из Стандарта?
Если уж и говорить о полном незнании языка, то его показываете именно вы: предложили пример, который работает не так, как вы задумывали, и вы до сих пор этого не видите, не говоря уже о том, что не понимаете сопутствующей проблемы в этом месте у компилятора clang.
Вы оказались не способны разобраться в причине, по которой clang отбрасывает первую шаблонную функцию, хотя я вам дважды намекал и говорил, что у других компиляторов тоже такая проблема была, но потом её исправили.
На godbolt'е же это легко проверить, "пощёлкав" версиями компиляторов.
У меня есть проверка возможности инстанциирования.
Вы применяете термин не по назначению, инстанцирование — это о другом. Видимо, поэтому у вас наблюдаются трудности с этим примером.
Вы определяете тип prvalue expression, а не создаёте временный объект.
И если prvalue expression — валидно, то разве у него может быть невалидый тип?
В одном из своих вариантов я доработал ваш пример так, чтобы невалидный тип (void &
) возникал, когда требуется.
И ваш пример стал работать так, как вы задумывали.
На всех компиляторах.
Так в чём суть? Что делает
void{}
?
Надеюсь, теперь вам это понятно.
А также, что вам понятно, кто из нас здесь показывает "полное незнание языка".
А
void()
— нет?
Я вам ссылку на Стандарт привёл и даже процитировал.
Похоже, вы на самом деле до сих пор не понимаете, в чём дело.
И статистика по тому, какие круги широкие, а какие — узкие, у вас… откуда?
Можете считать, как хотите.
Я всего лишь задаю вам наводящие вопросы.
Наверное, уже нет необходимости.
Прощу прощенья за то, что у меня пропущена частица «не», что, впрочем, должно было быть очевидно из противопоставления «если вы считаете, что
void()
что-то там инициализирует, аvoid{}
— нет».
Никакого противоречия не вижу.
Хорошо, первая часть фразы:
Пока что вы показываете полное незнание языка, если для вас
void()
иvoid{}
одно и то же
Не одно и то же?
Или опять частица «не» оказалась пропущенной?
А что до второго пункта — как считаете, какая часть практикующих программистов (ну, чтобы круги широкие были) знает про эту особенность?
Ну, вот, начались оправдывания...
Так что, есть у меня знания языка или нет?
Или я демонстрирую полное его незнание, как вы пишете?
А может, всё-таки здесь кто-то другой чего-то не знает?
Пример работает ровно так, как задумывалось: он показывает, что поведение с
void
неконсистентно между разными компиляторами уже сейчас, поэтому закладываться на него нельзя.
Нет, вы считали, что clang работает правильно, а gcc — нет:
Забавно, что gcc вполне себе жрёт
typename = decltype(void{})
, и то, что всех это на практике устраивает, как бы намекает нам, что всем пофиг.
Однако, — как раз, наоборот, gcc работает правильно, а clang — неправильно.
Какое-то время назад неправильно работал и gcc, и MSVC.
Во всех основных компиляторах, кроме clang'а, это исправили, думаю, в скором времени, и в clang'е исправят.
И ещё, вы писали:
Ох лол, я сконструировал такой пример
Пример имелся ввиду такой:
боюсь, что уже имеется очень много шаблонного кода, который опирается на то, что void — incomplete-тип (не инстанцируемый), и этот код сломается
Это — по поводу того, как он задумывался.
Что значит «невалидный тип»?
Я же вам привёл пример с void &
.
Моя вторая доработка вашего примера работает как раз на основе него.
Да, не понимаю. Вы «исправили» мой код, заменив
{}
на()
, а потом приводите ссылку на Стандарт, говорящий, что «{}
или()
приводит к такому-то результату» (подразумевая, что разницы нет). И я вот не понимаю: вас здесь ничего не смущает?
Меня — нет, не смущает.
По Стандарту разницы — нет, но clang — глючит, и поэтому при его использовании разница появляется.
Я вам это уже не первый раз объясняю, а вы всё понять не можете.
Именно, не одно и то же (в данном контексте).
Не часто удаётся встретить человека, спорящего со Стандартом.
Правда, ещё непонятно, зачем вы «исправили» мой код на
()
,
Чтобы clang перестал глючить и заработал согласно Стандарта.
Причём тут оправдывания? Это ровно тот сорт специальных случаев и исключений, про которые я говорил в самом начале.
При том, что это именно вы утверждали про меня:
Пока что вы показываете полное незнание языка
однако, на поверку, не я, а вы это показываете.
А теперь ещё и спорите со Стандартом.
И, кстати, что же вы тут имели в виду под инстанцированием?
Очевидно, создание объектов.
Так код уже сломан на практике. Он в разных компиляторах и в разное время показывает разные результаты.
На данный момент сломан clang, а не код, но вам, похоже, — все равно, ведь вам ваши хотелки важнее.
А можно я тоже ворвусь с вопросом. Мне вот тоже не очевидно даже после прочтения куска стандарта что
void ()
void{}
Это одно и тоже. А именно мне не понятно как компилятор парсит случай с (). Вообще говоря это не однозначная с точки зрения синтаксиса конструкция т.к. это может быть и выражение инициализации рвалью войд (или ещё по другому это можно считать войд литералом) так и сигнатурой функции которая ничего не принимает и ничего не возвращает. Единственное что могу предположить, что токены внутри деклвал трактуются как выражение == инициализация, а не как тип.
Вообще говоря это не однозначная с точки зрения синтаксиса конструкция т.к. это может быть и выражение инициализации рвалью войд (или ещё по другому это можно считать войд литералом) так и сигнатурой функции которая ничего не принимает и ничего не возвращает.
Верно.
Единственное что могу предположить, что токены внутри деклвал трактуются как выражение == инициализация, а не как тип.
Да, всё верно, и косвенно это можно увидеть из сообщений об ошибках компиляторов.
Для данного примера:
using fun = void();
using x = decltype(void());
using y = decltype(fun);
using z = decltype(void (*)());
генерируются типичные сообщения об ошибках:
<source>:4:20: error: unexpected type name 'fun': expected expression
4 | using y = decltype(fun);
| ^
<source>:5:27: error: expected expression
5 | using z = decltype(void (*)());
| ^
2 errors generated.
или:
<source>(4): error C3553: decltype expects an expression not a type
<source>(5): error C2760: syntax error: ')' was unexpected here; expected 'expression'
<source>(5): error C2059: syntax error: ')'
<source>(5): error C2059: syntax error: ')'
<source>(5): error C2059: syntax error: ')'
Для 3-ей строки никакой ошибки не сгенерировано.
А в сгенерированных сообщениях об ошибках прямо и недвусмысленно указано: "expected expression" и даже ещё более доходчиво: "decltype expects an expression not a type".
Спасибо за разъяснения, а то самому лень было смотреть.
Вы хорошо разбираетесь в с++ (и уверен не только в нем), но довольно грубо и высокомерно доносите свою позицию - это если что был сейчас комплимент. Я думаю что понимаю цену и причины подобного стиля общения т.к. сам кажется примерно такой же. Вы мне нравитесь и я бы с вами вероятно сработался.
Стройная модель не нарушается: если хотим, чтобы итерирование по адресам не приводило к непонятным багам, значит запрещаем увеличивать указатель на 0 при итерациях.
Раз что-то там запрещаем для конкретного случая, значит стройная модель нарушается, потому что сейчас всё работает без всяких "подтычек".
Сходу не могу придумать пример. Подскажете?
Что-нибудь такое:
#include <cstddef>
class B {
};
template <typename T>
struct D: B {
typedef T type;
};
template <typename T>
typename T::type &foo(T const &);
char (&foo(B const &))[100];
std::size_t bar() {
return sizeof foo(D<char[4]>{}) +
sizeof foo(D<void>{});
}
В данном случае функции даже определять не требуется.
Когда шаблонная функция foo
должна вернуть ссылку на void
, ищется следующая подходящая перегрузка, и она находится.
Если void
сделать "инстанцируемым", после чего станут возможны ссылки на void
, выбираться будет шаблонная функция вместо нешаблонной.
Контекст здесь — невычислимый (потому что под sizeof
'ом), поэтому определять функции не обязательно.
Нешаблонная функция foo
возвращает ссылку на массив из 100 char
'ов.
Функция bar
возвращает 104.
Раз что-то там запрещаем для конкретного случая, значит стройная модель нарушается
Это аналог запрета деления на 0. Никто ж не говорит, что он рушит арифметику. Наоборот, он позволяет объяснить, почему нельзя обратить умножение на 0. Тут же запрет будет ограничивать итерации по коллекциям элементов 0-го размера через указатели.
Что-нибудь такое
Можно ли пример, у которого есть шанс встретиться в реальном коде? А то как будто специально сначала расставляем грабли с одноимённым foo в разных контекстах, потом на них наступаем.
Что далеко ходить, код
return std::is_constructible<void>();
раньше возвращал 0, а будет возвращать 1. Изменение? Но корректное! Я могу сказать, что ваш пример из той же серии, просто обфусцированный. То есть, изменение полностью корректное и ничего не ломает.
Это аналог запрета деления на 0. Никто ж не говорит, что он рушит арифметику. Наоборот, он позволяет объяснить, почему нельзя обратить умножение на 0. Тут же запрет будет ограничивать итерации по коллекциям элементов 0-го размера через указатели.
Это — не эквивалентные вещи.
При делении на 0 не получается представить результат.
И запрета на деление на 0 — нет, это — UB, а не "запрет".
А при прибавлении 0 к указателю результат — представим.
И никакого UB там не возникает.
Можно ли пример, у которого есть шанс встретиться в реальном коде?
Чем этот пример не реален?
А то как будто специально сначала расставляем грабли с одноимённым foo в разных контекстах, потом на них наступаем.
Вы говорили:
Сходу не могу придумать пример. Подскажете?
Я привёл вам пример, из которого видна суть, а теперь вы привередничаете.
В реальном коде будет эксплуатироваться та же самая суть.
раньше возвращал 0, а будет возвращать 1. Изменение? Но корректное!
Какое бы оно ни было, корректное или нет, но оно приведёт к тому, что старый код начнёт работать по-другому.
Я могу сказать, что ваш пример из той же серии, просто обфусцированный. То есть, изменение полностью корректное и ничего не ломает.
В старом коде начнут вызываться не те функции — это ничего не ломает?
А при прибавлении 0 к указателю результат — представим.
И никакого UB там не возникает.
В предположении, что sizeof(void)==0
void A[7];
void* p1 = &(A[1]);
void* p2 = &(A[5]);
ptrdiff_t delta = p2-p1; // UB?
Логично делать UB, либо вообще запретить разность. А если разность запрещена, то и обратная операция - тоже.
Чем этот пример не реален?
Тем, что он взят не из реального проекта.
Я привёл вам пример, из которого видна суть, а теперь вы привередничаете
Суть не видна. Зачем такой код нужен в реальном проекте?
В старом коде начнут вызываться не те функции — это ничего не ломает?
Я скрафтил другой пример
return std::is_constructible<void>();
Функция стала возвращать другой результат. Но меня это не смущает, я считаю, что изменение не ломает реальные программы.
Логично делать UB
Да, логично, и некоторые компиляторы, поддерживающие соответствующее расширение, даже пишут об этом в предупреждении:
<source>:12:22: warning: subtraction of pointers to type 'void0' of zero size has undefined behavior [-Wpointer-arith] 12 | ptrdiff_t delta = p2-p1; // UB?
для этого кода:
#include <cstdlib>
#include <iostream>
typedef struct {
std::byte a[0];
} void0;
int main() {
void0 A[7];
void0* p1 = &(A[1]);
void0* p2 = &(A[5]);
ptrdiff_t delta = p2-p1; // UB?
std::cout << "delta: " << delta << std::endl;
return EXIT_SUCCESS;
}
Вы обнаружили ещё один прикол, который возникает в адресной арифметике, если сделать размер типа равным 0.
Ещё один аргумент против размера типа, равного 0.
А если разность запрещена, то и обратная операция - тоже.
А вот это — неверно.
Вы же сами приводили пример с делением: делить на 0 нельзя, но умножать-то на него, то есть, выполнять обратную операцию, — можно.
Так и здесь.
Тем, что он взят не из реального проекта.
Там будет эксплуатироваться то же самое.
Суть не видна.
Ну, если суть не видна, то ничем помочь не могу.
Функция стала возвращать другой результат. Но меня это не смущает, я считаю, что изменение не ломает реальные программы.
А реальные программы рассчитаны на то, что функция начнёт возвращать другой результат?
Сколько программ рассчитано на то, что sizeof(char)
может быть не равно 1?
Здесь, примерно, — то же самое.
sizeof(char)
по определению равно 1, в отличие от.
Собеседник предложил код:
return std::is_constructible<void>();
и сказал:
"раньше возвращал 0, а будет возвращать 1".
Что тогда мешает в таком же стиле изменить sizeof(char)
?
Примерно одного порядка вещи.
Я поэтому и задал этот вопрос, чтобы понятно было, насколько это нельзя менять.
Вы, возможно, имели в виду «сколько программ расчитано на возможность
CHAR_BITS
не быть равным 8» — и таких программ больше нуля.
Нет, я имел ввиду ровно то, что написал.CHAR_BITS
существует ещё с древних времён C.
Вы же сами приводили пример с делением: делить на 0 нельзя, но умножать-то на него, то есть, выполнять обратную операцию, — можно
Тут согласен, в этой аналогии прибавление любой целочисленной константы не должно двигать указатель. Я пытался максимально обезопасить итерации через сравнение с end-указателем, перестарался.
А реальные программы рассчитаны на то, что функция начнёт возвращать другой результат?
Если где-то и написан осмысленный код на std::is_constructible<void>
то только из-за того, что void-случаи требуют отдельной реализации (как в данной статье). Сам код вероятно не сломается, потому что пойдёт по ветке T - полноценный тип, в том числе и для void. В этом и цель предложения - убрать избыточность/копипасту.
Сколько программ рассчитано на то, что sizeof(char) может быть не равно 1?
Очень много. Нигде нет умножения на sizeof(char)
в конструкциях типа
char buf[100];
fread(buf, 1, 100, file);
Ну, если суть не видна, то ничем помочь не могу.
А вам видна? Вы понимаете практическую пользу от кода, который был вами приведён как пример "поломки"?
Если где-то и написан осмысленный код на
std::is_constructible<void>
то только из-за того, что void-случаи требуют отдельной реализации (как в данной статье). Сам код вероятно не сломается, потому что пойдёт по ветке T - полноценный тип, в том числе и для void. В этом и цель предложения - убрать избыточность/копипасту.
А если void
использовался как placeholder для incomplete типа, и взят был именно void
, чтобы он случайно не мог быть доопределён и поэтому не мог бы стать complete-типом?
Решения, базирующиеся на этом, сломаются, если void
сделать complete-типом.
Очень много. Нигде нет умножения на
sizeof(char)
в конструкциях типа
То есть, очень мало, а не очень много: практически никто не считает, что это может измениться.
Вот и с void
'ом — так же: никто не считает, что void
может стать complete-типом.
Вы понимаете практическую пользу от кода, который был вами приведён как пример "поломки"?
Не весь код в мире пишу я: есть ещё очень много других людей, которые тоже пишут код.
И понимать они практическую пользу могут очень по-своему.
По этой причине все вопросы вида "зачем так писать" и похожие по смыслу, например, о понимании практической пользы, бессмысленны.
А если void использовался как placeholder для incomplete типа, и взят был именно void, чтобы он случайно не мог быть доопределён
Например, если взять какой-то шаблон property, который создаёт operator=
только тогда, когда тип свойства позволяет создать такой оператор... Но теперь void может быть типом свойства, а значит, это уже не тип-плейсхолдер. Такой код действительно нужно будет пересматривать.
То есть, очень мало, а не очень много
Да, верно. Я неправильно понял.
Не весь код в мире пишу я: есть ещё очень много других людей, которые тоже пишут код.
Но этот код написали вы, и должны понимать, есть ли какой-то реальный смысл в такой конструкции. Всё выглядит так, что вы решали задачу "придумать пример, который покажет изменение поведения при внедрении предложения по void". То есть, просто обфусцированный is_constructible
Но этот код написали вы, и должны понимать, есть ли какой-то реальный смысл в такой конструкции.
Точнее, реальное применение.
Но здесь это — необязательно.
Всё выглядит так, что вы решали задачу "придумать пример, который покажет изменение поведения при внедрении предложения по void".
Да, верно.
Писатели очень изобретательны, они и без меня изобретут множество смыслов и применений.
Мне же здесь достаточно показать механизм, который они будут использовать для своих изобретений, которые сломаются, если void
сделать complete-типом.
Адресная арифметика работает с
void*
? Если не работает, то как это «никакого запрета нет»?
А вот так: условия не соблюдены.
Представьте себе, что завтра в стандарт добавят отдельную явную фразу «адресная арифметика с
void*
запрещена». Сломает ли это хоть какой-то существующий код?
Нет.
И это косвенно говорит о том, что сейчас запрета нет.
Стандарт вам уже прямо сейчас указывает так не делать. Будет указывать дальше, в чём вопрос?
Я же — о случаях, когда Стандарт разрешает.
А она действительно неважна постольку, поскольку наблюдаемое поведение компилятора такое же.
Об этом — ниже.
Окей, напишите программу, где работает адресная арифметика с
void
, раз она нигде не запрещена.
Так условия не соблюдены.
Вы продолжаете утверждать, что причина неважна, и поэтому ходите в этом месте по кругу.
Когда условия теоремы не соблюдены, теорема не применима.
Также и с адресной арифметикой: если условия для неё не соблюдены, она — не применима.
Дописали в стандарт фразу «
void
is a complete type, its size is zero, but pointer arithmetic withvoid*
is prohibited». В чём конкретная проблема с таким решением?
Уже обсуждалось: разные объекты могут иметь как разные адреса, так и одинаковые, а в последнем случае их нельзя отличить.
Тогда уже вводите и несравнимость адресов для void
.
Не считаю это меньшей проблемой,
Большинство считает, поэтому и не соглашается на предлагаемые вами изменения в Стандарт.
И невозможность их соблюсти означает, что запрет есть.
Совершенно не означает.
Через реку нет моста, человек может переплыть, а машина пересечь реку не может.
Означает ли это, что для машин есть запрет на пересечение реки в этом месте?
Думаю, сами понимаете абсурдность такой трактовки.
Если построить мост, то для машин появится возможность пересечь реку в данном месте, но это не означает, что для машин был снят запрет на это пересечение.
Потому что запрета и не было, была другая причина наблюдаемого не пересечения машинами реки до постройки моста.
Думаю, к этой части больше возвращаться не потребуется.
Значит, какая разница в наблюдаемом поведении?
Никакой.
Не понял, как отсутствие наблюдаемых изменений от явного добавления запрета говорит о том, что сейчас запрета нет?
А почему отсутствие наблюдаемых изменений должно говорить о наличии или отсутствии именно запрета, если возможны другие причины, кроме запрета?
Я выше привёл пример с мостом через реку, который это объясняет.
про самоприсваивание вы
слилисьвспомнили про шаблонные функции и в ответ на просьбу привести пример сказали «очень смешно».
Я не поддался на провокацию.
Что тут приводить?
Какая другая потребность различать объекты типа
void
у вас осталась?
Если шаблонная функция перед присваиванием проверяет, не самоприсваивание ли это, путём сравнения адресов, то, будучи инстанцированной для типа void
, в некоторых случаях для разных объектов void
она будет считать, что самоприсваивание есть, а в некоторых, — что его нет.
Если при этом, в случае, как она считает, отсутствия самоприсваивания, она выполняет дополнительные вещи, которые важны, то вот здесь и возникает проблема.
Это — то, что первое пришло в голову, то есть, могут быть и более "впечатляющие" случаи, но уже одного этого достаточно.
Такую фундаментальную вещь, как уникальность адреса для объектов любых типов, трогать нельзя.
Она настолько фундаментальная, что верна даже для функций.
Можно ссылку на это большинство?
Как вы себе это представляете?
Есть куда более сильная вещь, чем ссылка, — реальность.
Вы — в курсе, что предложения подобного рода уже были, и они не прошли?
Это и говорит о том, что "большинство считает".
Такую фундаментальную вещь, как уникальность адреса для объектов любых типов, трогать нельзя.
Это наблюдаемое частое, а не фундаментальное свойство.
Например, константные строки могут иметь один в могут иметь разный адрес. И сравнение их через сравнение указателей может работать а может нет. И проблема не в компиляторе, когда он сворачивает или нет константы, а в программисте, который полагается на свою логику а не факты
Это наблюдаемое частое, а не фундаментальное свойство.Например, константные строки могут иметь один в могут иметь разный адрес.
Очевидно, здесь имеются ввиду строковые литералы.
Вы когда-нибудь задумывались о том, что такое строковый литерал?
Вы уверены в том, что строковый литерал — это объект?
Если обратиться к первоисточнику, то можно узнать, что:
Никакой не объект, а — выражение.
И далее, первоисточник, 14-ый пункт (в самом низу):
Evaluating a string-literal results in a string literal object with static storage duration, initialized from the given characters as specified above.
Видите, строковый литерал вычисляется (Evaluating a string-literal), потому что это — выражение, а не объект.
А вот вычисление этого выражения уже даёт объект (results in a string literal object).
И — далее, там же:
Whether all string-literals are distinct (that is, are stored in nonoverlapping objects) and whether successive evaluations of a string-literal yield the same or a different object is unspecified.
Прямейшим текстом ещё раз написано, что строковые литералы именно вычисляются (successive evaluations of a string-literal), и что не специфицировано, дают ли последовательные вычисления строкового литерала один и тот же объект или нет (yield the same or a different object is unspecified).
Соответственно, из того, что строковые литералы при их последовательном их вычислении могут давать один и тот же объект, не следует, что адреса разных объектов могут быть равны.
И сравнение их через сравнение указателей может работать а может нет.
Верно, но, как видите, это — отнюдь не потому, что адреса разных объектов могут быть равны, а потому что строковые литералы при их вычислении могут давать один и тот же объект.
И проблема не в компиляторе, когда он сворачивает или нет константы, а в программисте, который полагается на свою логику а не факты
Верно, факты я вам привёл.
Проблема же, как вы говорите, — в программисте, который полагается на свою логику, а не на факты, и который думает, что разные объекты могут в некоторых случаях иметь один и то же адрес.
Вам же всю дорогу говорят, что нет у вас никаких «разных» объектов — void
, буде он таковым, — будет один. То, что вы пытаетесь его засунуть в разные места массива не делает его «разными объектами».
Вам же всю дорогу говорят, что нет у вас никаких «разных» объектов —
void
, буде он таковым, — будет один.
Мало ли, что говорят.
То, что вы пытаетесь его засунуть в разные места массива не делает его «разными объектами».
Я же уже приводил вам пример, как так может быть.
Вот вам наглядный пример, который показывает, благодаря наличию соответствующего расширения в компиляторах, что он не может быть один.
В первом массиве объекты void
неразличимы по адресам, что видно по распечатке их одинаковых адресов, и можно считать, что объект — один.
Во втором массиве их адреса уже различаются, потому что объекты находятся в составе структуры, в которой есть ещё поля с ненулевым размером, поэтому — уже не один.
В рамках этой аналогии — да, есть.
Вы по-видимому интерпретируете «запрет» как «явно написали, что конкретно так делать нельзя». Это слишком узкая трактовка, особенно в случае разных стандартов и прочих логических систем.
Посмотрите значение слова "запретить" в толковом словаре, например, здесь.
…в рамках вашей слишком узкой трактовки это высказывание имеет смысл, в рамках «запрещено всё, чья невозможность логически следует из стандарта» — смысла в нём нет.
Это — не моя трактовка, просто таково значение этого слова.
Проблема возникает, когда такую функцию (которая делает не связанные напрямую с совпадением адресов вещи) вы написали, а не когда её вызвали. Зачем вы пишете изначально сломанный код?
Разве я один пишу весь код в мире?
Напишут и вас не спросят.
Именно поэтому вопросы "зачем" в это контексте не имеют смысла.
Расскажите это принявшим
[[no_unique_address]]
, а то они не знают, наверное.
Знают, но по краю ходят.
Во-первых, это касается очень узкого круга случаев.
Во-вторых, это надо сознательно включить, то есть, сознательно вписать этот атрибут.
Если кто-то сознательно хочет нарушить уникальность адресов ради экономии памяти, зная, что в этом месте ему эта уникальность не нужна, то — вот, он получил "ручку" для того, чтобы это сделать.
На ранее написанный код это, в принципе, повлиять не может.
А, то есть, вы себе это не представляете, но про большинство сказали? Лол.
Так это же вы предлагали дать ссылку, а не я.
Один "лол" у вас уже был, правда, вы так и не поняли в тот раз, что это был за "лол".
Я в курсе, что был Matt Calabrese, которому я лично написал емейл с вопросом о его пропозале по regular void, и который сказал, что у него руки не доходят приехать на заседание Комитета представить свой пропозал. Если на заседании Комитета пропозал никто не представляет, то его не рассматривают изначально, и о «не прошли» (и о мнении большинства) тут говорить вообще нельзя.
Очевидно, большинство не очень хочет такое предлагать, энтузиазма не видно.
Мы ведь оба понимаем, что это не так? Шаблонную функцию, написанную заранее и давно, которая принимает тип
T
с полямиfoo
иbar
и ожидает, что у них разные адреса, и которая сломается от типа с такой аннотацией, вы можете себе представить?
У меня написано:
Если кто-то сознательно хочет нарушить уникальность адресов ради экономии памяти, зная, что в этом месте ему эта уникальность не нужна
Вы не заметили мою фразу "зная, что в этом месте ему эта уникальность не нужна", верно?
Если тип T
с полями foo
и bar
передаётся в шаблонную функцию, которая ожидает, что у них разные адреса, то это ведь — не тот случай, для которого верно условие "зная, что в этом месте ему эта уникальность не нужна"?
И — не важно, прямо эта уникальность ожидается или косвенно, посредством давно написанной шаблонной функции.
Так какие операции уместны для типа с нулевым размером
Пустое множество операций уместно ))
Какова "польза" от наличия такого типа?
Так вы за веткой не следите. Я начал этот холивар, чтобы унифицировать void-функции и функции со значением. Чтобы например при кодогенерации не делать 2 обёртки типа
template<TResult, TArgs...>
TResult wrapper(TArgs...) {
log(args);
TResult r = innerFunc(args);
return r;
}
template<TArgs...>
void wrapper_no_result(TArgs...) {
log(args);
innerFunc(args);
}
В C# аналогично - лямбды со значением (Func) и void-лямбды (Action) имеют различные типы, и весь код приходится писать дважды, чтобы поддержать и первые, и вторые.
В этом польза от void как обычного типа!
Перевешивает ли она вред от потери возможности различать объекты по адресам?
Да вроде никто не отбирает эту возможность, всё что раньше работало - продолжит работать.
Пустое множество операций уместно ))
То есть, сам по себе тип — бесполезен.
Так вы за веткой не следите. Я начал этот холивар, чтобы унифицировать void-функции и функции со значением. Чтобы например при кодогенерации не делать 2 обёртки типа
template<TResult, TArgs...>
TResult wrapper(TArgs...) {
log(args);
TResult r = innerFunc(args);
return r;
}
А для чего здесь искусственная промежуточная переменная r
?
Вот — другой, универсальный вариант, работающий и для void
'а:
template<TResult, TArgs...>
TResult wrapper(TArgs...) {
log(args);
return innerFunc(args);
}
Так — нельзя?
В этом польза от void как обычного типа!
Как видите, для получения универсального варианта нет необходимости в том, чтобы void
стал "обычным" типом.
Да вроде никто не отбирает эту возможность, всё что раньше работало - продолжит работать.
Если адреса различных объектов типа void
могут быть равны, а это именно так и будет, если его размер сделать равным 0, то возможности различать объекты типа void
по адресам не будет.
То есть, сам по себе тип — бесполезен
Сам по себе бесполезен. Как тип для подстановки в шаблонный код на место возвращаемого типа из функции - очень даже полезен.
А для чего здесь искусственная промежуточная переменная r?
потому что реальный код может быть:
TResult r = innerFunc(args);
log(r);
return r;
или даже
TResult r;
try {
r = innerFunc(args);
} catch (...) {
log(args);
throw;
}
return r;
Вот — другой, универсальный вариант, работающий и для void'а:
Вот это полезный пример. Не знал, что такой синтаксис допустим.
возможности различать объекты типа void по адресам не будет.
"никто не отбирает эту возможность" - значит, со старым кодом, с непустыми объектами, всё будет работать как работало.
Сам по себе бесполезен. Как тип для подстановки в шаблонный код на место возвращаемого типа из функции - очень даже полезен.
Это и сейчас возможно.
потому что реальный код может быть:
Передать void
в функцию нельзя, поэтому в таком случае не получится.
или даже
TResult r;
try {
r = innerFunc(args);
} catch (...) {
log(args);
throw;
}
return r;
А вот в этом случае — всё получится, можно упростить:
try {
return innerFunc(args);
} catch (...) {
log(args);
throw;
}
Вот это полезный пример. Не знал, что такой синтаксис допустим.
Так в статье же это описано.
"никто не отбирает эту возможность" - значит, со старым кодом, с непустыми объектами, всё будет работать как работало.
Но зато проблемы будут с новым.
Передать void в функцию нельзя, поэтому в таком случае не получится.
А если бы void был обычным типом, для которого логгер был бы специализирован писать строку "void", то получилось бы. И не нужно было бы писать 2 варианта обёрток.
Но зато проблемы будут с новым
Не могу придумать случай, когда нужно отличать меджу собой пустые объекты. Если задумка в том, чтобы создавать пустые объекты в куче и их адреса использовать как уникальные идентификаторы, то std::atomic<long>::fetch_add(1)
справится намного эффективнее.
Не могу придумать случай, когда нужно отличать меджу собой пустые объекты. Если задумка в том, чтобы создавать пустые объекты в куче и их адреса использовать как уникальные идентификаторы,
Нет, всё проще.
Например, может быть уже написан шаблонный код, который использует технику пробега по массиву указателями, который работает для всех типов, кроме предлагаемого вами void
.
Или используется range-based for
для массивов.
Получается, неразличимость объектов по адресам ломает обратную совместимость.
Скажем, так: вы опоздали с этим предложением, потому что огромное количество подобного кода уже написано.
Кстати, используя расширения компиляторов, можно посмотреть, как всё будет выглядеть:
#include <cstdlib>
#include <cstdint>
#include <iostream>
typedef union {
std::byte z[1];
} void1;
typedef union {
std::byte z[0];
} void0;
template <typename T, std::size_t N>
void fun(T (&a)[N]) {
std::cout << "\nsizeof a: " << sizeof a
<< "\nstd::size(a): " << std::size(a)
<< '\n';
std::cout << "\nLoop 1:\n";
for (auto &item: a) {
std::cout << &item << std::endl;
}
std::cout << "\nLoop 2:\n";
for (std::size_t i{}; i < std::size(a); ++i) {
std::cout << &a[i] << std::endl;
}
std::cout << std::endl;
}
int main() {
void1 a1[5];
void0 a0[5];
std::cout << "void1:\n=====\n";
fun(a1);
std::cout << "void0:\n=====\n";
fun(a0);
return EXIT_SUCCESS;
}
Видите, Loop 1 не отработал для void0
?
Обратная совместимость.
ЗЫ. Используя это расширение, вы вполне можете попробовать реально поэкспериментировать со своим вариантом типа void
.
ЗЗЫ. Интересно, что часть компиляторов, кроме gcc, могут выделить максимальное возможное количество элементов:
delete[] new void0[SIZE_MAX];
а gcc — не может даже такой:
delete[] new void0[SIZE_MAX / 2];
Пишет:
<source>:44:38: error: size '0' of array exceeds maximum object size '9223372036854775807' 44 | delete new void0[SIZE_MAX / 2];
Понимаете, 0 превышает максимальный размер объекта 9223372036854775807.
Какой у них огромный 0...
Или используется range-based for для массивов.
Видите, Loop 1 не отработал для void0?
С этим вообще нет проблем, если будет void[N] будет введено в стандарт. Т.к. компилятор знает N и может создать цикл на N итераций.
Понимаете, 0 превышает максимальный размер объекта 9223372036854775807
Компилятор глючит. Это ничего не говорит о самой идее.
С этим вообще нет проблем, если будет void[N] будет введено в стандарт. Т.к. компилятор знает N и может создать цикл на N итераций.
Да, специализированные итераторы и так далее.
Я даже специально попробовал, чтобы убедиться.
Существующий range-based for
это не вылечит, но циклы на итераторах работают.
Но с уже давно написанным шаблонным кодом проектов и библиотек, который явно использует указатели в качестве итераторов, проблемы останутся.
Думаю, это — вполне аргумент.
Компилятор глючит. Это ничего не говорит о самой идее.
Да, это — о качестве кода компилятора gcc, а не об идее.
Хорошо. Я согласен на sizeof(void)==1
, но доступ к этому значению - no-op, и поэтому компилятор вправе не занимать регистр при отпимизации ф-ций с типом void. Так будут и кошки сыты, и мыши целы.
Хорошо. Я согласен на
sizeof(void)==1
, но доступ к этому значению - no-op, и поэтому компилятор вправе не занимать регистр при отпимизации ф-ций с типом void.
Так это так и происходит с типом boid
, описанным в статье.
Напишите функцию, в которую передаются объекты типа boid
, в функции присваиваются один другому и возвращается какой-нибудь из них, например, которому присвоили.
Посмотрите, какой получается ассемблерный код.
Там будет всегда одна инструкция ret
.
Я думаю, что аргумент против изменения поведения типа void
будет именно такой, что то, что вы хотите, эмулируется пустой структурой с соответствующими конструкторами, операторами "равно" и деструктором.
Я думаю эта очевидная идея не пришла в голову только лишь вам. Я не знаю, но предполагаю, что ее реализация сложнее/хуже в реалиях 70х годов чем запрет на значение. У вас есть ллвм можете сами попробовать сделать Си где войд размера, 0 но имеет значение и посмотреть сколько проблем вылезет по дороге.
Будет отличное чтиво если ещё и статью по результатам запилите.
Бессмысленно. Мне это нужно не для теоретический изысканий, а для применения на практике. Даже если эксперимент "взлетит", C/C# уже не изменятся.
Мне кажется ваши комментарии выше в этой ветке исключительно теоретические т.к. в принципе не способные что то изменить - даже мнение других людей по этому вопросу. Следуя логике комментария на который я отвечаю это бессмысленно.
Возникает противоречие либо все ваши коменты в этой ветке бессмысленны либо все таки смысл создания такого языка есть, но вам этого делать не хочется. Это нормально, но тогда не удивляйтесь, что вашу идею с 0 размером войда никто серьезно не воспринимает т.к. обоснованно опасаются за проблемы которые это несёт. Вы утверждаете, что проблем нет коллективное мы вам не верим, мысленного экспиремента, даже не эксперимента, а просто обрывков рассуждений не достаточно.
Цель моей активности в этой ветке - собрать аргументы разработчиков против значений с нулевым размером. Возможно, это обогатит мои знания, если найдутся интересные кейсы. Пока нашлось только "так нельзя, потому что нельзя" и "значения, даже пустые, обязательны должны иметь разные адреса, потому что... ну, потому что".
Хотя, вот обнаружил занятный синтаксис
void some_func() {
return other_func();
}
Интересно, в формальной грамматике справа от этого return что - "значение"? Тогда void - полноценное значение (rvalue), как минимум. Или эта конструкция в грамматике прописана как исключение.
В си постоянно обнаруживаются неожиданные синтаксические конструкции.
Ну например
char str[] = "xz";
void* ptr = str;
const void xz;
*ptr = xz;
Что показывает этот пример?
По моему предложению, чтение-запись void - это no-op, поэтому
*ptr = xz;
ничего не делает.
В общем то сейчас ровно то же самое за исключением того что хранить значение войд нельзя. Создавать можно - (void)0, читать/писать тоже можно - дефолтовый конструктор и любые функции без пераметров. Только хранить нельзя. Почему нельзя я не знаю, но раз все остальное можно, а хранить нельзя очевидно была причина иначе бы сделали. Поэтому вам нужно сначало найти причину почему хранить войд значение нельзя, а уже имея причину можно понять почему размер 0 не поможет.
Наверное правило любой лвалью должно иметь свой адрес не такое уж и глупое и для чего то нужно, а не просто так. Я думаю это суть проблемы. Вероятно отсутствие различимого адреса как то сильно усложняет код ген или анализ или оптимизации или вообще жесткое требование абстрактной машины в чьи инструкции все преобразуется.
Я так понимаю, всё ради удобства.
auto ptr = malloc(100);
ptr += 50;
В случае нулевого void'а -- ptr не изменяется. В случае void'а считаемого в 1 байт -- ptr указывает на середину теперь.
Да, можно делать через касты:
ptr = (void*)((char *)ptr + 50);
Но это может поломать имеющийся код. Хотя в чем проблема просто ругаться на арифметику с void'ами не только под -Wpedantic
-- не представляю. Скорее всего, лень.
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0146r1.html
Многа букав, но там есть хороший разбор альтернатив и одна из алтернатив как раз 0 размер и описание что поламается - итераторы.
Папир не принят и я не смог найти что с ним не так и почему не принят.
auto ptr = malloc(100);
ptr += 50;
Сейчас не работает.
https://godbolt.org/z/1ooondMr6
Опасения в том, что оно вдруг заработает?
Работает, в gcc.
Интересно, это UB или ошибка компилятора...
https://gcc.gnu.org/onlinedocs/gcc/Pointer-Arith.html -- и оно там с доисторических времён, возможно, даже с прошлого тысячелетия... почему?
Да, это "расширение" стандарта как они считают.
В коде это вот тут: https://github.com/gcc-mirror/gcc/blob/885143fa77599c44bfdd4e8e6b6987b7824db6ba/gcc/c-family/c-common.cc#L3378
Прошелся по истории, появилось это в
https://gcc.gnu.org/git/?p=gcc.git;a=commit;f=gcc/cp/typeck.c;h=8d08fdba598cf87c3794df53beae1026345ebb02
То есть это еще с 1994го года, со времён свинины... "From-SVN: r6613"
тут пишут, что это всё-таки нарушение Стандарта компилятором
Это не может быть нарушением Стандарта.
Как только компилятор используется в режиме с расширениями, так он сразу же перестаёт быть компилятором C/C++ и становится компилятором некоторого языка, похожего на C/C++, на который даже стандарта нет.
Вот если бы он так себя вёл в режиме без расширений, тогда — да, было бы нарушение Стандарта.
Точно не знаю когда там bottom type и unit type придумали. Точно знаю они уже были такие в Haskell, но он сильно позже Си был создан.
Но по факту, void это не полноценный unit type. В полноценном должна быть возможность создавать значение этого типа, просто оно единственное. Так что void получится где-то между unit и bottom type, вторым он тоже, очевидно, не является. Иначе бы void функции никогда бы не возвращали управление, а они фактически же возвращают то самое значение void, но его нельзя при этом присвоить переменной.
Вот такие вот пироги.
Всё это, конечно, занимательно. Но, почему не был сделан немедленный откат супер-фреймворка?!
А как же классический if constexpr? Или enable_if, или requires на сам метод test_running_time? Тогда и boid не потребовался бы. Или у этого типа есть ещё какой то смысл?
Так а в чем была разница в тех 50 тестах которые так и не починились сразу? Неужто не интересно?
В феврале-мае 23 в google bench внесли изменения, которые в некоторых конфигурациях тестов приводили к результату void. Но в предыдущей версии это войд ловился самим фреймворком и возвращался enum { ResultError }, а в новой void просто прокидывался дальше. Для исправления пришлось бы переписывать эти тесты под новый интерфейс, что потянуло бы изменения в зависимостях уже в тестах игры и движка, что по времени получалось немало, поэтому решили подхачить бенчмарк и не переписывать кучу кода.
Я не знаю историю появления типа
void
в языке, но было бы интересно узнать, возможно кто-нибудь в коментариях и напишет.
Говорят, что Стивен Борн (автор Bourne Shell) еще задолго до ANSI C предложил Деннису Ритчи включить void в язык, т.к. ему не нравилось, что нельзя было создать функции, которые ничего не возвращают (до этого по умолчанию они должны были возвращать значения типа int).
Само слово из Алгола 68, спецификацию к которому в свое время писал Борн.
Вот ссылка на quora.com там есть два видео, где он об этом рассказывает. (возможно потребуется VPN).
На том же сайте Стив Джонсон еще рассказал, как появился указатель на void в качестве замены char*, который возвращала malloc().
Напомните мне, в шутке про отстрел ноги речь про какой тип оружия была?
Прочитал статью, прочитал комментарии... Не знаю, возможно, дело в том, что читаю спросонья, но не понимаю ни сути описанной в статье проблемы, ни предложенного решения, ни смысла в существовании самой статьи, пардон май френч.
Почему нельзя было сделать так:
class test_duration_meter {
const pcstr m_test_name;
const time_point m_started_at;
public:
explicit test_duration_meter(pcstr test_name)
: m_test_name{test_name}
, m_started_at{gtl::high_resolution_clock::now()}
{}
~test_duration_meter() {
const auto finished_at = gtl::high_resolution_clock::now();
gtl::duration<double> diff = finished_at - m_started_at;
test_printf("Test %s spent time %ds \n", m_test_name, diff.count());
}
};
template <typename F, typename... Args>
auto test_running_time(pcstr test_name, F&& f, Args&&... args)
{
test_duration_meter test_duration{test_name};
return gtl::invoke(gtl::forward<F>(f), gtl::forward<Args>(args)...);
}
По сути этот вариант эквивалентен SCOPE_EXIT.
Автор пишет что такой вариант помог, но еще оставались некомпилируемые тесты
Показанный в статье вариант со SCOPE_EXIT -- это творение какого-то сумрачного разума, в котором invoke
вынуждены вызывать дважды, причем в первом случае все равно зачем-то делают сохранения результата вызова в auto result
.
Так что нет, не вижу эквивалентности.
Пардонь-те, может не правильно понял проблему, но:
#include <iostream>
template<typename Fn, typename... Args>
auto Run(Fn f, Args... args) -> std::invoke_result_t<Fn, Args...> {
if constexpr (std::is_same<std::invoke_result_t<Fn, Args...>, void>::value) {
std::cout << "pre-run" << std::endl;
f(args...);
std::cout << "post-run" << std::endl;
} else {
std::cout << "pre-run" << std::endl;
auto res = f(args...);
std::cout << "post-run" << std::endl;
return res;
}
}
int f1() { std::cout << "f1" << std::endl; return 1; }
std::string f1s(std::string v) { std::cout << "f1-string " << v << std::endl; return v + v; }
int f2(int k) { std::cout << "f2 - " << k << std::endl; return 2; }
void f2s(std::string v) { std::cout << "f2-string " << v << std::endl; }
int main()
{
std::cout << Run(f1) << std::endl;
std::cout << Run(f1s, std::string("--")) << std::endl;
std::cout << Run(f2, 7) << std::endl;
Run(f2s, std::string("++"));
return 0;
}
Получаемый результат:
pre-run
f1
post-run
1
pre-run
f1-string --
post-run
----
pre-run
f2 - 7
post-run
2
pre-run
f2-string ++
post-run
Профит? Нет?
Прим.: комментарии все не осилил, может что-то не учёл.
А перегрузить шаблон на void нельзя было?
Ну всмысле специализацию сделать.
1) Административное решение. Не принимать библиотеку в ветку qa-checked или как оно там называется в вашем репозитории. А если прямо совсем мешает разработке, то отревертить в вашей основной ветке - мастер/транк/как оно там называется.
Пусть техлид отведёт фичную ветку, где займётся миграцией на новую версию библиотеки в спокойном режиме.
Как вообще можно было вмержить в мастер эпохальное изменение, минуя тесты? Или часть библиотек живёт вне репозитория? Ну хотя бы докерфайл же должен жить в репе? Значит, надо переделывать, чтобы системные зависимости тоже там оказались.
2) Выкиньте свой boid, вы рискуете отхватить проблемы в самых неожиданных местах, где будет делаться проверка на void / не-void. Потому что вжух, и там окажется не-void.
Кстати, зачем у boid конструктор от более чем одного аргумента, да ещё и с приплясыванием "кроме наследников boid"? Чую, у вас там где-то говнокод с оператором запятая притаился, который оказался вовсе не оператором...
А не хотите ли проверить, что где-то запятая ведёт себя не так, как вы ожидаете?
3) Специализация / if-constexpr в общем случае, плюс разные техники scope guard там, где они применимы.
4) Фактически, вам очень жирно подсветили, что именно ваша обвязка не умеет работать с void. Заодно проверьте, умеет ли она корректно работать с другими разнообразными типами. Ссылочные, некопируемые, неполные, - вот это всё.
Вот прямо сходу бросается в глаза:
template <typename F, typename... Args>
auto test_running_time(pcstr test_name, F&& f, Args&&... args) {
.....
auto result = gtl::invoke(gtl::forward<F>(f), gtl::forward<Args>(args)...);
.....
return result;
}
Локальная переменная по значению, и возврат по значению. Знаете, сколько граблей тут рассыпано? Часть из них можно было бы убрать с помощью decltype(auto)
.
5) Юниттесты на вашу библиотеку тестов есть? Они покрывают это разнообразие?
Void me