void
в плюсах довольно забавная штука. Мы можем привести к void
почти любой тип, завести указатель с типомvoid*
, который может адресовать что угодно. Еще можем сделать функцию с возвращаемым типом void
, которая ничего не возвращает. Объявление функции типа void f(void)
будет просто функцией без аргументов. Но вот иметь объекты типа void
или написать что-то вроде void&
не можем. Это немного странно, но не настолько, чтобы вызывать у вас бессонные ночи, пока вы не начинаете ловить странные баги, когда void вообще не void.
Проблема возникла где не ждали, а именно на проекте немного обновили бенчмарк фреймворк, казалось что такого может случиться на выполнении тестов?
"Ничего хорошего не случится" - сказал техлид и в пятницу вечером залил, в обход этих самых тестов, новый фреймворк. А сам укатил на какую-то конференцию.
"Приехали, у нас лапки" - сказали QA team в понедельник, когда ни один из тестов не запустился. Здесь и далее gtl это namespace для кастомной реализации std в движке XXX, от game templates library, почти полностью совместимый по интерфейсам с std/eastl. A код был примерно таким:
template <typename F, typename... Args>
auto test_running_time(pcstr test_name, F&& f, Args&&... args) {
auto start = gtl::high_resolution_clock::now();
auto result = gtl::invoke(gtl::forward<F>(f), gtl::forward<Args>(args)...);
auto end = gtl::high_resolution_clock::now();
gtl::duration<double> diff = end - start;
test_printf("Test %s spent time %ds \n", test_name, diff.count());
return result;
}
Здесь нет, вернее не было, ошибок. Код делает то, что нам нужно, а именно берет два временных отсчёта до и после вызова функции, получает разницу в секундах, пишет её куда-то во внутренние логи и возвращает результат обратно в вызвавшую функцию. И это работало для всех случаев, пока не обновили google benchmark. После обновления сломались функции, которые возвращают void
— для них компилятор выдаёт ошибку, указывая на объявление переменной result
.
траля-ля-ля, две простыни логов с ворнингами в шаблонах и вот она ошибка
...
error: 'void result' has incomplete type
// тут еще много текста и наконец
auto result = gtl::invoke(/* ... */)
^~~~~~
"Да ну вас нафик, какие ошибки с войдом" - говорите вы и лезете смотреть, что же там такого поломалось. А просто версия фреймворка поломалась, т.е. обновилась, и придется теперь с этим жить. Сначала попробуем в лоб - просто ходим по тестам и перегружаем test_running_time()
для тех функций, которые возвращают void
, дублируем все тело функции, в надежде что рабочий день скоро закончится. Но проблема в том, что тестов у нас больше 8к, и около десяти процентов из них сломаны. Это раздражает и после 10 замены вы понимаете, что Сизиф катит этот камень куда-то не туда. И ладно бы с копипастой и временем, но представьте что вам еще и ревью защищать, а там копипасту ох как не любят, так что в целом этот путь полностью провальный — дублировать каждый шаблон функции опасно для ментального здоровья ваших коллег.
Немного поразмыслив над проблемой можно прийти к другому решению, чтобы вытащить тип результата из выражения, и сразу возвращать его без промежуточного хранения.
template <typename F, typename... Args>
auto test_running_time(pcstr test_name, F&& f, Args&&... args){
auto start = gtl::high_resolution_clock::now();
SCOPE_EXIT{
// auto result = gtl::invoke(gtl::forward<F>(f), gtl::forward<Args>(args)...);
auto end = gtl::high_resolution_clock::now();
gtl::duration<float> diff = end - start;
test_printf("Test %s spent time %ds \n", test_name, diff.count());
}
return gtl::invoke(gtl::forward<F>(f), gtl::forward<Args>(args)...);;
}
Как ни странно это сработало. Но если подумать, то никаких секретов тут нет, мы просто воспользовались деструктором локальной переменной, которая будет вызвана сразу после выхода из функции, т.е. это не нарушает обычный поток выполнения тестов, просто выглядит немного странно, при этом выполняет возложенную функцию на этот участок кода - измеряет время выполнения теста. Компилятор не смущает, что мы возвращаем объект типа void
, и хотя объекта типа void не существует, в компиляторе есть особое правило на этот случай, которое позволяет возвращать void как результат выполнения функции. Иначе бы не работала половина стандартной библитеки.
Это почти сработало, почти - потому что с полтинник тестов (но это уже была победа, всего пять десятков, а не восемь сотен) все равно не могли заинстанситься компилятором, и даже вернувшийся с конфУренции техлид разводил руками. Грустно посмотрев на это дело, закоментили эти тесты, чтобы не останавливать работу QA команды, пообщав им скоро это дело починить.
Закончился спринт...
Оставшиеся тесты не собирались - это мучало лида команды QA, он мучал вопросами своих подопечных, а те в свою очередь плакались разработчикам и обещали наловить багов побольше да позаковыристей, если последние эти тесты починят.
Так прошло пару недель, QA становились все более раздражительные, а проходя в курилку выразительно поглядывали на топор, который стоит перед дверью в отдел QA, как символ их незаменимого труда, видимо, чтобы мотивировать таким образом погромистов выполнить данное обещание.
В итоге мы с коллегами подумали и сделали тип, который бы заменил void
и имел схожий функционал и который бы можно свободно подставлять там где обычный void
не справлялся. Назвали незамысловато boid
, и пусть он значит немного другое, но по смыслу (шарик, что-то мелкое, что можно вернуть из функции) очень даже подходит. Пустой тип, который можно сконструировать из чего угодно. К нему идут две вспомогательных обертки, чтобы преобразовывать между собой, где это необходимо. Там где, компилятор не осилил возвратить void
— теперь возвращаетсяboid
, потому что он является настоящим объектным типом, который нормально умеет создаваться.
struct boid {
boid() = default;
boid(boid const&) = default;
boid(boid&&) = default;
boid& operator=(boid const&) = default;
boid& operator=(boid&&) = default;
template <typename Arg, typename... Args,
gtl::enable_if_t<!gtl::is_base_of_v<boid, gtl::decay_t<Arg>>, int> = 0>
explicit boid(Arg&&, Args&&...) { }
};
template <typename T>
using wrap_boid_t = gtl::conditional_t<gtl::is_void_v<T>, boid, T>;
template <typename T>
using unwrap_boid_t = gtl::conditional_t<gtl::is_same_v<gtl::decay_t<T>, boid>, void, T>;
Переписываем немного код функции, чтобы она умела работать с новым boid
типом, для этого понадобится еще две обертки, чтобы компилятор умел различать эти типы.
template <typename F, typename ...Args,
typename Result = gtl::invoke_result_t<F, Args...>,
gtl::enable_if_t<!gtl::is_void_v<Result>, int> = 0>
Result invoke_boid(F&& f, Args&& ...args) {
return gtl::invoke(gtl::forward<F>(f), gtl::forward<Args>(args)...);
}
template <typename F, typename ...Args,
typename Result = gtl::invoke_result_t<F, Args...>,
gtl::enable_if_t<gtl::is_void_v<Result>, int> = 0>
boid invoke_boid(F&& f, Args&& ...args) {
gtl::invoke(gtl::forward<F>(f), gtl::forward<Args>(args)...);
return Void();
}
У внимательного хабрачитателя вполне может возникнуть вопрос, почему бы просто не поменять возвращаемый тип в оставшихся тестах? Ответ простой - это не всегда было возможно и сложившиеся зависимости в тестах приводили к новым изменениям, которые приводили к другим изменениям, и так далее. Так что вооружившись новый boid'ом остается переписать немного исходный пример, и вуаля сломанные тесты вернулись в строй:
template <typename F, typename... Args>
auto test_running_time(pcstr test_name, F&& f, Args&&... args)
{
auto start = gtl::high_resolution_clock::now();
// gtl::invoke -> invoke_boid
auto result = invoke_boid(gtl::forward<F>(f), gtl::forward<Args>(args)...);
auto end = gtl::high_resolution_clock::now();
gtl::duration<double> diff = end - start;
test_printf("Test %s spent time %ds \n", test_name, diff.count());
return result;
}
Заключение
Всё это ненормальное программирование, с перегрузкой войда поднимает вопрос — а чегоvoid
вообще такой странный? Техлид говорил - «Потому что C», всегда ведь проще сказать, что виноват старый язык программирования, чем новомодный стандарт. Я не знаю историю появления типа void
в языке, но было бы интересно узнать, возможно, кто-нибудь в комментариях и напишет.
Думаю void
не является объектным типом не просто так, а чтобы нельзя было передать экземпляр void
в функцию, как тогда этот аргумент обрабатывать? Вопрос конечно интересный.
void foo(int a, void b, float c);
Комментарии (84)
stepsoft
21.10.2024 17:18А как же классический if constexpr? Или enable_if, или requires на сам метод test_running_time? Тогда и boid не потребовался бы. Или у этого типа есть ещё какой то смысл?
datacompboy
21.10.2024 17:18Так а в чем была разница в тех 50 тестах которые так и не починились сразу? Неужто не интересно?
dalerank Автор
21.10.2024 17:18В феврале-мае 23 в google bench внесли изменения, которые в некоторых конфигурациях тестов приводили к результату void. Но в предыдущей версии это войд ловился самим фреймворком и возвращался enum { ResultError }, а в новой void просто прокидывался дальше. Для исправления пришлось бы переписывать эти тесты под новый интерфейс, что потянуло бы изменения в зависимостях уже в тестах игры и движка, что по времени получалось немало, поэтому решили подхачить бенчмарк и не переписывать кучу кода.
lazy_val
21.10.2024 17:18У товарищей из гугла в github ни одного инцидента на тему
void result
не находится, ни в открытых, ни в закрытыхЛибо я плохо ищу, либо получается что вы одни на эту засаду налетели, либо не одни, но все остальные налетевшие тоже гордо промолчали
datacompboy
21.10.2024 17:18Я бы очень хотел взглянуть на пример теста, который может "приводить к результату void"...
Ну то есть тест нормальный, но внезапно овойдивается.
Serpentine
21.10.2024 17:18Я не знаю историю появления типа
void
в языке, но было бы интересно узнать, возможно кто-нибудь в коментариях и напишет.Говорят, что Стивен Борн (автор Bourne Shell) еще задолго до ANSI C предложил Деннису Ритчи включить void в язык, т.к. ему не нравилось, что нельзя было создать функции, которые ничего не возвращают (до этого по умолчанию они должны были возвращать значения типа int).
Само слово из Алгола 68, спецификацию к которому в свое время писал Борн.
Вот ссылка на quora.com там есть два видео, где он об этом рассказывает. (возможно потребуется VPN).
На том же сайте Стив Джонсон еще рассказал, как появился указатель на void в качестве замены char*, который возвращала malloc().
eao197
21.10.2024 17:18Прочитал статью, прочитал комментарии... Не знаю, возможно, дело в том, что читаю спросонья, но не понимаю ни сути описанной в статье проблемы, ни предложенного решения, ни смысла в существовании самой статьи, пардон май френч.
Почему нельзя было сделать так:
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)...); }
anz
21.10.2024 17:18По сути этот вариант эквивалентен
SCOPE_EXIT.
Автор пишет что такой вариант помог, но еще оставались некомпилируемые тестыeao197
21.10.2024 17:18Показанный в статье вариант со SCOPE_EXIT -- это творение какого-то сумрачного разума, в котором
invoke
вынуждены вызывать дважды, причем в первом случае все равно зачем-то делают сохранения результата вызова вauto result
.Так что нет, не вижу эквивалентности.
Apoheliy
21.10.2024 17:18Пардонь-те, может не правильно понял проблему, но:
#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
Профит? Нет?
Прим.: комментарии все не осилил, может что-то не учёл.
Gorthauer87
Просто сишники не особенно хорошо читали теорию типов
boldape
А эта теория уже была тогда? Ну так то да сишный войд это юнит, а невозможность инстанцирования вероятно сильно упрощает язык типа сколько байт занимает значение? А массив? А если это мембер структуры? А нужен ему адрес? А зачем? Да и кому вообще нужен массив/мемберы юнитов/войдов? Можно конечно заморочиться и сделать специальные оптимизации везде где встречается подобная дичь, но это как раз и есть усложнение компилятора. Можно ничего не делать, но тогда драгоценные байтики памяти/регистры для аргументов/результата тех времён расходуются. Думаю поступили практично нет
человеказначения нет проблем.qw1
Сделать sizeof(void) == 0 и всё.
Компилятор не пришлось бы усложнять, void-значение занимало бы 0 места в распределении регистров при передаче параметров или возврате результата.
Потом этот void проник в c# и мне в метапрограммировании приходится писать вдвое больше кода, отдельно для void-функций и обычных. Также, различать
Action<T>
иFunc<T,R>
Больше проблем, чем плюсов.
KanuTaH
Сомневаюсь. Возникли бы примерно те же проблемы, по причине которых
sizeof
пустой структуры в C++ составляет 1 байт а не 0.dalerank Автор
верно, будут проблемы, размер массива структур не может быть нулевым. Плюс если я правильно помню, нулевой и отрицательные размеры зарезерированы под определенные нужды компилятора.
Gorthauer87
Ну в Rust как-то со всем этим справились, там могут быть и типы нулевого размера и unit type можно присваивать переменной.
KanuTaH
Ну и как результат там плодятся "специальные случаи" - например, если засунуть unit type в Box а потом попытаться получить указатель не него - скажем через
Box::as_ptr()
, то результатом будет невалидный указатель. Потенциальные грабли, заботливо разложенные для пейсателей unsafe кода.qw1
Не вижу проблемы. Указатель типизированный, а значит мы знаем размер читаемой области. void* всегда может быть без проблем разыменован в пустое чтение, no-op. Невалидного void* указателя не существует. Вроде, никаких конфликтов с другими частями стандарта.
KanuTaH
Эхехе. Ну вот взять например тот же вызов malloc для void (ведь раз его можно разместить "на стеке", значит, и на куче можно, так?). Что должен вернуть malloc? Реально выделить память нулевого размера он не может. NULL означает ошибку. Сделать "специальный случай" и не вызывать malloc вовсе (примерно так делает и растовский Box)? Сработает, если ты контролируешь реализацию, как растовский std контролирует реализацию своего Box (и то с проблемами, когда дело доходит до получения raw указателя), не сработает для пользовательского кода (например, шаблонного). Потребуются приседания, в том числе и со стороны пейсателя кода, по сути аналогичные приседаниям автора статьи - вставить constexpr if, придумать отдельную ветку для такого случая, как-то откуда-то сделать какой-то указатель и т.п. А в C вообще пришлось бы приседать в рантайме, а не в compile time.
qw1
Логично было бы вернуть
nullptr
, но для malloc это значение зарезервировано под статус ошибки.Пускай возвращает implementation-defined константу, для которой free ничего не делает. И всё, никаких приседаний со стороны писателя кода.
В стандартной библиотеке добавится проверка на size=0, но в malloc как правильно она уже есть (выбор пула аллокации в зависимости от размера блока). Во free уже есть проверка на nullptr, если константа будет
(void*)-1
то изменение может быть бесплатным - вместоptr==0
будетptr<=0
KanuTaH
Ну т.е. одного NULL (про который многие считают, что и его-то быть не должно) уже мало, нужно еще какое-то зарезервированное значение. Поддержки со стороны одного лишь компилятора оказалось маловато. Я просто уверен, что с дальнейшим продвижением в лес количество дров будет возрастать и дальше. А со структурами с минимальным размером в 1 байт ничего из этого просто не нужно, все работает "естественным образом".
qw1
Это вообще никак не влияет на стандарт и пользователей библиотек/компиляторов. Это детали реализации stdlib.
Если не хотите оптимизации, пусть malloc выделяет область в 0 байт, делает перед ней обычные заголовки области, и отдаёт уникальный адрес. На другие сценарии не повлияет. А если кто-то написал
std::vector<void>(30)
ну пусть аллокация будет. Раньше это вообще не компилировалось. А теперь такой странный код (который обычно и не встретится) будет не оптимальным.
Кроме malloc (по которому вы меня не убедили), других возражений нет?
KanuTaH
Да, и я о чем. К типам нулевого размера нужно "приучать" не только компилятор, но и libc, а это, как вы справедливо заметили, отдельная от конпелятора вещь.
Он и сейчас может это делать - создавать non-dereferenceable область "в ноль байт" (на самом деле нет) с уникальным адресом, которую потом нужно "освободить" через вызов
free()
, а может вернутьNULL
. Это допустимое с точки зрения стандарта поведение.Ну если брать например раст, то там, как я уже выше говорил, используется подход с non-dereferenceable указателем (со значением
0x1
) для объектов нулевого размера, расположенных "в куче" (по крайней мере Box возвращает такой вот "указатель" на содержащийся в себе unit type). Это весьма error-prone - скажем, в libc'шномmemcpy()
такой "указатель" использовать нельзя вне зависимости от значения параметраcount
. Но это сейчас, а какие соображения были против zero-sized void в свое время у создателей C, чем они руководствовались - я не знаю, но, видимо, какие-то основания у них были.qw1
Аргумент из серии "так сложилось". Я не предлагаю переделать текущий стандарт (это уже невозможно), я утверждаю, что если бы изначально void был полноценным значением с размером 0, это было бы намного лучше, чем есть сейчас. Естественно, тогда бы и libc писался под такие реалии.
Если бы изначально в стандарте был 0-байт void, memcpy делал бы no-op при count=0. Да и сейчас, не думаю, что где-то реально для оптимизации используется ограничение, что при count=0, указатели должны быть валидными.
То есть, это просто замечание, а не аргумент против.
KanuTaH
Ну это не то чтобы аргумент против, просто я к тому, что в реальности размер все равно будет не 0, так зачем притворяться, что он 0. Вот реализация пустых структур в C++ и решила не притворяться.
qw1
Вы говорите, что malloc(0) потратит не 0? На что я могу возразить, что malloc(17) потратит не 17 байт, а больше.
Причём, есть реальная возможность чтобы malloc(0) тратил 0, но вы всеми лапами упираетесь против такой оптимизации libc, приплетая сюда какой-то "второй NULL" (который будет неизвестен никому, кроме free от libc, а значит, не потребует нигде ещё особого обращения).
Опять же, вы зацепились за malloc. В других же местах будет 0 байт, хоть структура, хоть массив из 100 void-ов.
KanuTaH
И в чем тут "возражение"? Это взаимодополняющие утверждения, я бы сказал.
Ну это не то чтобы я упираюсь, а сами libc по большей части. Ведь
malloc()
сам по себе не в курсе целевого типа, а только лишь необходимого размера, что ему мешало раньше применять такую "оптимизацию" при выделении блоков нулевого размера? Но как-то я в распространенных реализациях libc такой "оптимизации" не встречал. Видимо, на то есть причины.Ну фактически тоже нет. Возьмем тот же раст, в котором "со всем этим справились" (нет). Уникальные адреса на стеке? Да. Значит, фактически размер уже не может быть "0 байт".
qw1
А что будет, если это требование не выполнить?
KanuTaH
Пока не попробуешь - не узнаешь :)
P.S. Ну вообще в релизе по крайней мере в данном конкретном (я бы сказал, простейшем) случае оптимизатор это дело оптимизирует и размещает обе структуры по одному адресу. Но это именно что оптимизация, которая по сути своей опциональна и в отладочной сборке не выполняется. Т.е. никаких гарантий по факту раст на эту тему не предоставляет.
qw1
За исключением того, что если нужно какое-то метапрограммирование, нужно отдельно описывать случай void-функции и не-void.
KanuTaH
Это касается только и непосредственно самого
void
. Если на местеvoid
будет пустая структура, то из-за ее ненулевого размера никаких специальных приседаний с ней не требуется совершенно.qw1
Всё верно, но реальный код, который пишут реальные программисты, имеет функции с типом void, а не с пустой структурой. Игнорировать void-функции невозможно в универсальных инфраструктурных библиотеках.
Gorthauer87
Ну так это примерно как получить указатель на константу или пустой слайс, это вообще ортогональные проблемы.
qw1
Что случится, если разрешить нулевой размер структур?
Ввели ограничения для своего удобства, а потом оправдывают этим принципиальную невозможность
KanuTaH
Именно что для удобства, причём не только и не столько своего.
А кто говорит о принципиальной невозможности? По-моему никто. Все говорят исключительно о цене реализации этих хотелок.
qw1
Пока ни одного примера, где рост цены неизбежен.
KanuTaH
Предлагаемые вами выкрутасы со "вторым NULL" - это не рост цены?
qw1
Это не "второй NULL".
Там, где нужны проверки на NULL, они остаются без изменений и проверяется только обычный NULL. Проверка на заглушку нужна только на free, и она может быть бесплатной, как я показал раньше. Если не хочется отсекать половину адресного пространства (если у нас x86, а не x64), можно взять значение
(void*)1
и тогда проверка на входеfree
вместо
eptr
Каждый элемент массива, в том числе, из
void
'ов, должен иметь уникальный адрес.С адресной арифметикой знакомы?
Можно дальше не объяснять?
Именно такова причина, по которой размер типа данных не может быть равен 0.
KanuTaH
Ну так-то справедливости ради нужно сказать что адресную арифметику с
void*
в стандартных C и C++ использовать нельзя, можно только в расширениях, в частности от GNU. В Cvoid
- это, как там, "incomplete type that cannot be complete" или как-то так, поэтому его размер неизвестен и известен быть не может, какая уж тут адресная арифметика.eptr
Верно, но речь-то о том, что (я отвечал на эту цитату):
После этого препятствий для самой адресной арифметики нет.
qw1
Сможете объяснить, почему?
Да
Нужно объяснять. Потому что все считают это очевидным, а как сформулировать словами, так впадают в ступор.
eptr
Для различения объектов при доступе к ним через указатели.
Вообще, первая же найденная мной ссылка по данному вопросу описывает несколько вариантов, в которых возникли бы проблемы, если бы тип был нулевого размера.
В ступор впадают не все.
Адрес следующего элемента в массиве отличается от адреса текущего на размер элемента. Если размер элемента равен 0, то адреса всех элементов в массиве будут иметь один и тот же адрес, и их невозможно будет различить по адресам.
KanuTaH
Ну, этими аргументами вы не проймете :) В расте, например, такой код будет работать по-разному будучи собранным в дебаге и релизе, но это никого из апологетов раста не смущает, все привыкли, дескать, "а чего вы хотите от объектов с нулевым размером", как в анекдоте про доктора, который отвечает пациенту "а вы так не делайте". И, в принципе, можно сказать, что они в своем праве, для них это нечто вроде вкусовщины.
eptr
Абсолютных аргументов здесь и нет, это — вопрос выбора.
Что характеризует выбор, сделанный в Rust'е.
Однако, так — нечестно, так и в C/C++ адреса разные будут.
Вот так — куда честнее.
Первый раз писал на Rust'е, и больше не хочу. Компилятор мне ещё указывает, в каком регистре идентификаторы заводить.
Отлично, ребята сделали себе игрушку, но почему им теперь обязательно нужно "осчастливливать" таким же выбором C/C++?
Апологеты C/C++, почему-то не пытаются осчастливливать Rust.
qw1
void - не объект.
Не вижу проблем, чтобы все экземпляры void были isSame.
Очевидно, если программист захочет различать экземпляры, он не будет заводить их с типом void.
И следствие какое из этого? Пример какой-нибудь, поближе к практике...
eptr
А что это тогда?
Они в принципе не могут всегда быть одним экземпляром, пример я приводил здесь.
Следствием является то, что адреса всех элементов в массиве становятся равны и поэтому не различимы по адресу.
По той ссылке, которую я раньше приводил, есть ещё примеры проблем, возникающих из-за одинаковости адресов различных объектов.
qw1
В си различаются простые типы (int, char) и составные (struct, class). Объекты экземпляры сложных типов. В-общем, не принципиально, вопрос определений.
Я написал не точно, вы дали бесполезный (для меня) ответ.
Если уж совсем строго, моя реплика должны была быть такой:
Точно есть? И вы согласны с тем, что это реальные проблемы?
У void нет методов - нет проблем.
Если представить объект, состоящий из void-ов, то у объекта нет состояния, значит и поведение всех экземпляров одинаковое. Если это разные типы, а вызов виртуальный, значит есть vptr и объект уже не нулевого размера.
Про malloc/free выше 100500 сообщений было. Нет там никаких проблем.
_countof для массива из void не скомпилируется из-за деления на 0. Подумаешь, пользовательский макрос не компилируется. Тут нужны примеры, где это могло встретиться в реальном коде. Размер массива знаем при его объявлении. Не хочется дублировать выражение - можно сделать constexpr.
eptr
Статья — по C++.
Вы — в курсе, что методы — это синтаксический сахар, и на самом деле вызывается функция, которой первым параметром передаётся ссылка на объект?
У объектов пустого класса тоже нет состояния, и — что?
Для примеров необходимо отдельно исследовать вопрос, чтобы найти что-то менее тривиальное.
Пока всё довольно бесполезное и потенциально опасное, например, станут возможны ссылки на
void
, поскольку теперь можно будет разыменовать указатель наvoid
, но эти к этим ссылкам нельзя будет обращаться.qw1
В плюсы пока не завезли методы для простых типов, так что с этой стороны они пока ещё не объекты. Или можно писать
(42).to_string()
, я что-то пропустил?Для пустых объектов без разницы, на каком экземпляре вызывать метод, метод не может работать с данными объекта, поэтому совпадение адресов не ломает ничего.
В чём опасность, если обращение к void - это no-op?
Mingun
Это просто следствие того, что потребовали, что каждый элемент массива должен иметь свой адрес. Чем это требование обосновано? Видимо, пока его писали, просто забыли, что элемент может иметь нулевой размер, автоматических решалок-то ограничений еще не было, никто противоречивость и не заметил
eptr
Как минимум тем, что иначе, как я написал выше, будет невозможно различать различные объекты по адресам.
Сомневаюсь, что забыли и не заметили.
Если вернуться к основам, то тип характеризуется набором операций, определённых над объектами этого типа, а если это ещё и тип данных, то — и размером.
С размером — всё понятно, он равен 0.
Какие операции были бы уместны для объектов такого типа?
Mingun
Так объект нулевого размера всегда один, что там различать?
Операции -- ну, вот в том же Rust какие-то операции нашлись?
eptr
Это он в одном и том же массиве — "один".
А, например, в такой структуре:
Адреса
&s.v0
и&s.v1
, очевидно, будут различными, и это будут разные объекты.Rust — это Rust, а здесь — C++.
Так какие операции уместны для типа с нулевым размером, кроме взятия адреса и операции "запятая" (которая, кстати, и сейчас работает для выражений типа
void
)?Какова "польза" от наличия такого типа?
Перевешивает ли она вред от потери возможности различать объекты по адресам?
Mingun
А адреса
&s
и&s.v0
будут одинаковыми, но снова очевидно, что это разные объекты. И что?Для начала надо бы понять, зачем нам вообще различать объекты по адресам. Что это дает? Если сами объекты неразличимы, то зачем требовать, чтобы их адреса различались?
Второе, зачем нам требовать, чтобы объекты нулевого размера вели себя всегда точно также, как объекты ненулевого. Ноль -- уже само по себе особое число, почему требование равнять его на других? В математике вы же не требуете, например, чтобы умножение 0 на 1 давало 1, хотя все остальные числа при умножении на 1 дают 1. Почему с размерами типов должно быть по другому?
eptr
Адреса массива и его первого элемента тоже совпадают.
s.v0
— подобъект объектаs
, здесь нет ничего удивительного.Добавьте ещё оно поле ненулевого размера перед полем
v0
, и тогда адресаs
иs.v0
перестанут совпадать.К обсуждаемому это не относится.
Речь шла о том, один ли объект нулевого размера, или их может быть несколько.
Вы утверждали, что — один, я показываю, что их может быть несколько.
Например, можно защититься от присваивания самому себе.
Если этого — мало, можно почитать эту ветку на SO.
Для начала ответьте на вопросы:
и
А то вы обсуждаете, как должен быть введён этот тип, забыв обдумать, а — нужен ли он такой вообще?
qw1
Пустое множество операций уместно ))
Так вы за веткой не следите. Я начал этот холивар, чтобы унифицировать void-функции и функции со значением. Чтобы например при кодогенерации не делать 2 обёртки типа
В C# аналогично - лямбды со значением (Func) и void-лямбды (Action) имеют различные типы, и весь код приходится писать дважды, чтобы поддержать и первые, и вторые.
В этом польза от void как обычного типа!
Да вроде никто не отбирает эту возможность, всё что раньше работало - продолжит работать.
eptr
То есть, сам по себе тип — бесполезен.
А для чего здесь искусственная промежуточная переменная
r
?Вот — другой, универсальный вариант, работающий и для
void
'а:Так — нельзя?
Как видите, для получения универсального варианта нет необходимости в том, чтобы
void
стал "обычным" типом.Если адреса различных объектов типа
void
могут быть равны, а это именно так и будет, если его размер сделать равным 0, то возможности различать объекты типаvoid
по адресам не будет.qw1
Сам по себе бесполезен. Как тип для подстановки в шаблонный код на место возвращаемого типа из функции - очень даже полезен.
потому что реальный код может быть:
или даже
Вот это полезный пример. Не знал, что такой синтаксис допустим.
"никто не отбирает эту возможность" - значит, со старым кодом, с непустыми объектами, всё будет работать как работало.
eptr
Это и сейчас возможно.
Передать
void
в функцию нельзя, поэтому в таком случае не получится.А вот в этом случае — всё получится, можно упростить:
Так в статье же это описано.
Но зато проблемы будут с новым.
qw1
А если бы void был обычным типом, для которого логгер был бы специализирован писать строку "void", то получилось бы. И не нужно было бы писать 2 варианта обёрток.
qw1
Не могу придумать случай, когда нужно отличать меджу собой пустые объекты. Если задумка в том, чтобы создавать пустые объекты в куче и их адреса использовать как уникальные идентификаторы, то
std::atomic<long>::fetch_add(1)
справится намного эффективнее.boldape
Я думаю эта очевидная идея не пришла в голову только лишь вам. Я не знаю, но предполагаю, что ее реализация сложнее/хуже в реалиях 70х годов чем запрет на значение. У вас есть ллвм можете сами попробовать сделать Си где войд размера, 0 но имеет значение и посмотреть сколько проблем вылезет по дороге.
Будет отличное чтиво если ещё и статью по результатам запилите.
qw1
Бессмысленно. Мне это нужно не для теоретический изысканий, а для применения на практике. Даже если эксперимент "взлетит", C/C# уже не изменятся.
boldape
Мне кажется ваши комментарии выше в этой ветке исключительно теоретические т.к. в принципе не способные что то изменить - даже мнение других людей по этому вопросу. Следуя логике комментария на который я отвечаю это бессмысленно.
Возникает противоречие либо все ваши коменты в этой ветке бессмысленны либо все таки смысл создания такого языка есть, но вам этого делать не хочется. Это нормально, но тогда не удивляйтесь, что вашу идею с 0 размером войда никто серьезно не воспринимает т.к. обоснованно опасаются за проблемы которые это несёт. Вы утверждаете, что проблем нет коллективное мы вам не верим, мысленного экспиремента, даже не эксперимента, а просто обрывков рассуждений не достаточно.
qw1
Цель моей активности в этой ветке - собрать аргументы разработчиков против значений с нулевым размером. Возможно, это обогатит мои знания, если найдутся интересные кейсы. Пока нашлось только "так нельзя, потому что нельзя" и "значения, даже пустые, обязательны должны иметь разные адреса, потому что... ну, потому что".
Хотя, вот обнаружил занятный синтаксис
Интересно, в формальной грамматике справа от этого return что - "значение"? Тогда void - полноценное значение (rvalue), как минимум. Или эта конструкция в грамматике прописана как исключение.
В си постоянно обнаруживаются неожиданные синтаксические конструкции.
boldape
Ну например
qw1
Что показывает этот пример?
По моему предложению, чтение-запись void - это no-op, поэтому
ничего не делает.
boldape
В общем то сейчас ровно то же самое за исключением того что хранить значение войд нельзя. Создавать можно - (void)0, читать/писать тоже можно - дефолтовый конструктор и любые функции без пераметров. Только хранить нельзя. Почему нельзя я не знаю, но раз все остальное можно, а хранить нельзя очевидно была причина иначе бы сделали. Поэтому вам нужно сначало найти причину почему хранить войд значение нельзя, а уже имея причину можно понять почему размер 0 не поможет.
Наверное правило любой лвалью должно иметь свой адрес не такое уж и глупое и для чего то нужно, а не просто так. Я думаю это суть проблемы. Вероятно отсутствие различимого адреса как то сильно усложняет код ген или анализ или оптимизации или вообще жесткое требование абстрактной машины в чьи инструкции все преобразуется.
datacompboy
Я так понимаю, всё ради удобства.
В случае нулевого void'а -- ptr не изменяется. В случае void'а считаемого в 1 байт -- ptr указывает на середину теперь.
Да, можно делать через касты:
Но это может поломать имеющийся код. Хотя в чем проблема просто ругаться на арифметику с void'ами не только под
-Wpedantic
-- не представляю. Скорее всего, лень.boldape
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0146r1.html
Многа букав, но там есть хороший разбор альтернатив и одна из алтернатив как раз 0 размер и описание что поламается - итераторы.
Папир не принят и я не смог найти что с ним не так и почему не принят.
qw1
Хороший, но про 0-size там буквально 1 абзац.
Итераторы в std наверное можно специализировать, как итератор по
std::vector<bool>
ходит не по байтам, а по битам.qw1
Сейчас не работает.
https://godbolt.org/z/1ooondMr6
Опасения в том, что оно вдруг заработает?
datacompboy
Работает, в gcc.
qw1
Интересно, это UB или ошибка компилятора...
datacompboy
https://gcc.gnu.org/onlinedocs/gcc/Pointer-Arith.html -- и оно там с доисторических времён, возможно, даже с прошлого тысячелетия... почему?
qw1
тут пишут, что это всё-таки нарушение Стандарта компилятором, а не UB или IDB.
datacompboy
Да, это "расширение" стандарта как они считают.
В коде это вот тут: 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"
Gorthauer87
Точно не знаю когда там bottom type и unit type придумали. Точно знаю они уже были такие в Haskell, но он сильно позже Си был создан.
Но по факту, void это не полноценный unit type. В полноценном должна быть возможность создавать значение этого типа, просто оно единственное. Так что void получится где-то между unit и bottom type, вторым он тоже, очевидно, не является. Иначе бы void функции никогда бы не возвращали управление, а они фактически же возвращают то самое значение void, но его нельзя при этом присвоить переменной.
Вот такие вот пироги.