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)


  1. Gorthauer87
    21.10.2024 17:18

    Просто сишники не особенно хорошо читали теорию типов


    1. boldape
      21.10.2024 17:18

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


      1. qw1
        21.10.2024 17:18

        Сделать sizeof(void) == 0 и всё.
        Компилятор не пришлось бы усложнять, void-значение занимало бы 0 места в распределении регистров при передаче параметров или возврате результата.

        Потом этот void проник в c# и мне в метапрограммировании приходится писать вдвое больше кода, отдельно для void-функций и обычных. Также, различать Action<T> и Func<T,R>
        Больше проблем, чем плюсов.


        1. KanuTaH
          21.10.2024 17:18

          Сделать sizeof(void) == 0 и всё.

          Сомневаюсь. Возникли бы примерно те же проблемы, по причине которых sizeof пустой структуры в C++ составляет 1 байт а не 0.


          1. dalerank Автор
            21.10.2024 17:18

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


            1. Gorthauer87
              21.10.2024 17:18

              Ну в Rust как-то со всем этим справились, там могут быть и типы нулевого размера и unit type можно присваивать переменной.


              1. KanuTaH
                21.10.2024 17:18

                Ну и как результат там плодятся "специальные случаи" - например, если засунуть unit type в Box а потом попытаться получить указатель не него - скажем через Box::as_ptr(), то результатом будет невалидный указатель. Потенциальные грабли, заботливо разложенные для пейсателей unsafe кода.


                1. qw1
                  21.10.2024 17:18

                  Не вижу проблемы. Указатель типизированный, а значит мы знаем размер читаемой области. void* всегда может быть без проблем разыменован в пустое чтение, no-op. Невалидного void* указателя не существует. Вроде, никаких конфликтов с другими частями стандарта.


                  1. KanuTaH
                    21.10.2024 17:18

                    Не вижу проблемы

                    Эхехе. Ну вот взять например тот же вызов malloc для void (ведь раз его можно разместить "на стеке", значит, и на куче можно, так?). Что должен вернуть malloc? Реально выделить память нулевого размера он не может. NULL означает ошибку. Сделать "специальный случай" и не вызывать malloc вовсе (примерно так делает и растовский Box)? Сработает, если ты контролируешь реализацию, как растовский std контролирует реализацию своего Box (и то с проблемами, когда дело доходит до получения raw указателя), не сработает для пользовательского кода (например, шаблонного). Потребуются приседания, в том числе и со стороны пейсателя кода, по сути аналогичные приседаниям автора статьи - вставить constexpr if, придумать отдельную ветку для такого случая, как-то откуда-то сделать какой-то указатель и т.п. А в C вообще пришлось бы приседать в рантайме, а не в compile time.


                    1. qw1
                      21.10.2024 17:18

                      Логично было бы вернуть nullptr, но для malloc это значение зарезервировано под статус ошибки.

                      Пускай возвращает implementation-defined константу, для которой free ничего не делает. И всё, никаких приседаний со стороны писателя кода.

                      В стандартной библиотеке добавится проверка на size=0, но в malloc как правильно она уже есть (выбор пула аллокации в зависимости от размера блока). Во free уже есть проверка на nullptr, если константа будет (void*)-1 то изменение может быть бесплатным - вместо ptr==0 будет ptr<=0


                      1. KanuTaH
                        21.10.2024 17:18

                        Ну т.е. одного NULL (про который многие считают, что и его-то быть не должно) уже мало, нужно еще какое-то зарезервированное значение. Поддержки со стороны одного лишь компилятора оказалось маловато. Я просто уверен, что с дальнейшим продвижением в лес количество дров будет возрастать и дальше. А со структурами с минимальным размером в 1 байт ничего из этого просто не нужно, все работает "естественным образом".


                      1. qw1
                        21.10.2024 17:18

                        Это вообще никак не влияет на стандарт и пользователей библиотек/компиляторов. Это детали реализации stdlib.

                        Если не хотите оптимизации, пусть malloc выделяет область в 0 байт, делает перед ней обычные заголовки области, и отдаёт уникальный адрес. На другие сценарии не повлияет. А если кто-то написал
                        std::vector<void>(30)
                        ну пусть аллокация будет. Раньше это вообще не компилировалось. А теперь такой странный код (который обычно и не встретится) будет не оптимальным.

                        Кроме malloc (по которому вы меня не убедили), других возражений нет?


                      1. KanuTaH
                        21.10.2024 17:18

                        Это вообще никак не влияет на стандарт и пользователей библиотек/компиляторов. Это детали реализации stdlib.

                        Да, и я о чем. К типам нулевого размера нужно "приучать" не только компилятор, но и libc, а это, как вы справедливо заметили, отдельная от конпелятора вещь.

                        Если не хотите оптимизации, пусть malloc выделяет область в 0 байт, делает перед ней обычные заголовки области, и отдаёт уникальный адрес.

                        Он и сейчас может это делать - создавать non-dereferenceable область "в ноль байт" (на самом деле нет) с уникальным адресом, которую потом нужно "освободить" через вызов free(), а может вернуть NULL. Это допустимое с точки зрения стандарта поведение.

                        Кроме malloc (по которому вы меня не убедили), других возражений нет?

                        Ну если брать например раст, то там, как я уже выше говорил, используется подход с non-dereferenceable указателем (со значением 0x1) для объектов нулевого размера, расположенных "в куче" (по крайней мере Box возвращает такой вот "указатель" на содержащийся в себе unit type). Это весьма error-prone - скажем, в libc'шном memcpy() такой "указатель" использовать нельзя вне зависимости от значения параметра count. Но это сейчас, а какие соображения были против zero-sized void в свое время у создателей C, чем они руководствовались - я не знаю, но, видимо, какие-то основания у них были.


                      1. qw1
                        21.10.2024 17:18

                        К типам нулевого размера нужно "приучать" не только компилятор, но и libc, а это, как вы справедливо заметили, отдельная от конпелятора вещь

                        Аргумент из серии "так сложилось". Я не предлагаю переделать текущий стандарт (это уже невозможно), я утверждаю, что если бы изначально void был полноценным значением с размером 0, это было бы намного лучше, чем есть сейчас. Естественно, тогда бы и libc писался под такие реалии.

                        Это весьма error-prone - скажем, в libc'шном memcpy() такой "указатель" использовать нельзя вне зависимости от значения параметра count

                        Если бы изначально в стандарте был 0-байт void, memcpy делал бы no-op при count=0. Да и сейчас, не думаю, что где-то реально для оптимизации используется ограничение, что при count=0, указатели должны быть валидными.

                        Он и сейчас может это делать - создавать non-dereferenceable область "в ноль байт" (на самом деле нет) с уникальным адресом, которую потом нужно "освободить" через вызов free(), а может вернуть NULL. Это допустимое с точки зрения стандарта поведение

                        То есть, это просто замечание, а не аргумент против.


                      1. KanuTaH
                        21.10.2024 17:18

                        То есть, это просто замечание, а не аргумент против.

                        Ну это не то чтобы аргумент против, просто я к тому, что в реальности размер все равно будет не 0, так зачем притворяться, что он 0. Вот реализация пустых структур в C++ и решила не притворяться.


                      1. qw1
                        21.10.2024 17:18

                        Вы говорите, что malloc(0) потратит не 0? На что я могу возразить, что malloc(17) потратит не 17 байт, а больше.

                        Причём, есть реальная возможность чтобы malloc(0) тратил 0, но вы всеми лапами упираетесь против такой оптимизации libc, приплетая сюда какой-то "второй NULL" (который будет неизвестен никому, кроме free от libc, а значит, не потребует нигде ещё особого обращения).

                        Опять же, вы зацепились за malloc. В других же местах будет 0 байт, хоть структура, хоть массив из 100 void-ов.


                      1. KanuTaH
                        21.10.2024 17:18

                        Вы говорите, что malloc(0) потратит не 0? На что я могу возразить, что malloc(17) потратит не 17 байт, а больше.

                        И в чем тут "возражение"? Это взаимодополняющие утверждения, я бы сказал.

                        Причём, есть реальная возможность чтобы malloc(0) тратил 0, но вы всеми лапами упираетесь против такой оптимизации libc, приплетая сюда какой-то "второй NULL" (который будет неизвестен никому, кроме free от libc, а значит, не потребует нигде ещё особого обращения).

                        Ну это не то чтобы я упираюсь, а сами libc по большей части. Ведь malloc() сам по себе не в курсе целевого типа, а только лишь необходимого размера, что ему мешало раньше применять такую "оптимизацию" при выделении блоков нулевого размера? Но как-то я в распространенных реализациях libc такой "оптимизации" не встречал. Видимо, на то есть причины.

                        Опять же, вы зацепились за malloc. В других же местах будет 0 байт, хоть структура, хоть массив из 100 void-ов.

                        Ну фактически тоже нет. Возьмем тот же раст, в котором "со всем этим справились" (нет). Уникальные адреса на стеке? Да. Значит, фактически размер уже не может быть "0 байт".


                      1. qw1
                        21.10.2024 17:18

                        Уникальные адреса на стеке?

                        А что будет, если это требование не выполнить?


                      1. KanuTaH
                        21.10.2024 17:18

                        А что будет, если это требование не выполнить?

                        Пока не попробуешь - не узнаешь :)

                        P.S. Ну вообще в релизе по крайней мере в данном конкретном (я бы сказал, простейшем) случае оптимизатор это дело оптимизирует и размещает обе структуры по одному адресу. Но это именно что оптимизация, которая по сути своей опциональна и в отладочной сборке не выполняется. Т.е. никаких гарантий по факту раст на эту тему не предоставляет.


                      1. qw1
                        21.10.2024 17:18

                        со структурами с минимальным размером в 1 байт ничего из этого просто не нужно, все работает "естественным образом"

                        За исключением того, что если нужно какое-то метапрограммирование, нужно отдельно описывать случай void-функции и не-void.


                      1. KanuTaH
                        21.10.2024 17:18

                        За исключением того, что если нужно какое-то метапрограммирование, нужно отдельно описывать случай void-функции и не-void.

                        Это касается только и непосредственно самого void. Если на месте void будет пустая структура, то из-за ее ненулевого размера никаких специальных приседаний с ней не требуется совершенно.


                      1. qw1
                        21.10.2024 17:18

                        Всё верно, но реальный код, который пишут реальные программисты, имеет функции с типом void, а не с пустой структурой. Игнорировать void-функции невозможно в универсальных инфраструктурных библиотеках.


                1. Gorthauer87
                  21.10.2024 17:18

                  Ну так это примерно как получить указатель на константу или пустой слайс, это вообще ортогональные проблемы.


            1. qw1
              21.10.2024 17:18

              размер массива структур не может быть нулевым

              Что случится, если разрешить нулевой размер структур?

              зарезерированы под определенные нужды компилятора

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


              1. KanuTaH
                21.10.2024 17:18

                Ввели ограничения для своего удобства

                Именно что для удобства, причём не только и не столько своего.

                оправдывают этим принципиальную невозможность

                А кто говорит о принципиальной невозможности? По-моему никто. Все говорят исключительно о цене реализации этих хотелок.


                1. qw1
                  21.10.2024 17:18

                  Пока ни одного примера, где рост цены неизбежен.


                  1. KanuTaH
                    21.10.2024 17:18

                    Предлагаемые вами выкрутасы со "вторым NULL" - это не рост цены?


                    1. qw1
                      21.10.2024 17:18

                      Это не "второй NULL".
                      Там, где нужны проверки на NULL, они остаются без изменений и проверяется только обычный NULL. Проверка на заглушку нужна только на free, и она может быть бесплатной, как я показал раньше. Если не хочется отсекать половину адресного пространства (если у нас x86, а не x64), можно взять значение (void*)1 и тогда проверка на входе free

                          test ecx, -2
                          jz exit
                      

                      вместо

                          test ecx, ecx
                          jz exit
                      


        1. eptr
          21.10.2024 17:18

          Сделать sizeof(void) == 0 и всё.
          Компилятор не пришлось бы усложнять, void-значение занимало бы 0 места в распределении регистров при передаче параметров или возврате результата.

          Каждый элемент массива, в том числе, из void'ов, должен иметь уникальный адрес.

          С адресной арифметикой знакомы?
          Можно дальше не объяснять?

          Именно такова причина, по которой размер типа данных не может быть равен 0.


          1. KanuTaH
            21.10.2024 17:18

            Ну так-то справедливости ради нужно сказать что адресную арифметику с void* в стандартных C и C++ использовать нельзя, можно только в расширениях, в частности от GNU. В C void - это, как там, "incomplete type that cannot be complete" или как-то так, поэтому его размер неизвестен и известен быть не может, какая уж тут адресная арифметика.


            1. eptr
              21.10.2024 17:18

              В C void - это, как там, "incomplete type that cannot be complete" или как-то так, поэтому его размер неизвестен и известен быть не может, какая уж тут адресная арифметика.

              Верно, но речь-то о том, что (я отвечал на эту цитату):

              Сделать sizeof(void) == 0 и всё.

              После этого препятствий для самой адресной арифметики нет.


          1. qw1
            21.10.2024 17:18

            Каждый элемент массива, в том числе, из void'ов, должен иметь уникальный адрес.

            Сможете объяснить, почему?

            С адресной арифметикой знакомы?

            Да

            Можно дальше не объяснять?

            Нужно объяснять. Потому что все считают это очевидным, а как сформулировать словами, так впадают в ступор.


            1. eptr
              21.10.2024 17:18

              Сможете объяснить, почему?

              Для различения объектов при доступе к ним через указатели.

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

              Нужно объяснять. Потому что все считают это очевидным, а как сформулировать словами, так впадают в ступор.

              В ступор впадают не все.

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


              1. KanuTaH
                21.10.2024 17:18

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

                Ну, этими аргументами вы не проймете :) В расте, например, такой код будет работать по-разному будучи собранным в дебаге и релизе, но это никого из апологетов раста не смущает, все привыкли, дескать, "а чего вы хотите от объектов с нулевым размером", как в анекдоте про доктора, который отвечает пациенту "а вы так не делайте". И, в принципе, можно сказать, что они в своем праве, для них это нечто вроде вкусовщины.


                1. eptr
                  21.10.2024 17:18

                  Ну, этими аргументами вы не проймете :)

                  Абсолютных аргументов здесь и нет, это — вопрос выбора.

                  В расте, например, такой код будет работать по-разному будучи собранным в дебаге и релизе, но это никого из апологетов раста не смущает, все привыкли, дескать, "а чего вы хотите от объектов с нулевым размером", как в анекдоте про доктора, который отвечает пациенту "а вы так не делайте".

                  Что характеризует выбор, сделанный в Rust'е.

                  В расте, например, такой код

                  Однако, так — нечестно, так и в C/C++ адреса разные будут.
                  Вот так — куда честнее.

                  Первый раз писал на Rust'е, и больше не хочу. Компилятор мне ещё указывает, в каком регистре идентификаторы заводить.

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

                  Отлично, ребята сделали себе игрушку, но почему им теперь обязательно нужно "осчастливливать" таким же выбором C/C++?

                  Апологеты C/C++, почему-то не пытаются осчастливливать Rust.


              1. qw1
                21.10.2024 17:18

                Для различения объектов при доступе к ним через указатели

                void - не объект.

                Вообще, первая же найденная мной ссылка

                Не вижу проблем, чтобы все экземпляры void были isSame.
                Очевидно, если программист захочет различать экземпляры, он не будет заводить их с типом void.

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

                И следствие какое из этого? Пример какой-нибудь, поближе к практике...


                1. eptr
                  21.10.2024 17:18

                  void - не объект.

                  А что это тогда?

                  Не вижу проблем, чтобы все экземпляры void были isSame.

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

                  И следствие какое из этого? Пример какой-нибудь, поближе к практике...

                  Следствием является то, что адреса всех элементов в массиве становятся равны и поэтому не различимы по адресу.

                  По той ссылке, которую я раньше приводил, есть ещё примеры проблем, возникающих из-за одинаковости адресов различных объектов.


                  1. qw1
                    21.10.2024 17:18

                    А что это тогда?

                    В си различаются простые типы (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.


                    1. eptr
                      21.10.2024 17:18

                      В си различаются простые типы (int, char) и составные (struct, class).

                      Статья — по C++.

                      У void нет методов - нет проблем.

                      Вы — в курсе, что методы — это синтаксический сахар, и на самом деле вызывается функция, которой первым параметром передаётся ссылка на объект?

                      Если представить объект, состоящий из void-ов, то у объекта нет состояния, значит и поведение всех экземпляров одинаковое.

                      У объектов пустого класса тоже нет состояния, и — что?

                      Тут нужны примеры, где это могло встретиться в реальном коде.

                      Для примеров необходимо отдельно исследовать вопрос, чтобы найти что-то менее тривиальное.

                      Пока всё довольно бесполезное и потенциально опасное, например, станут возможны ссылки на void, поскольку теперь можно будет разыменовать указатель на void, но эти к этим ссылкам нельзя будет обращаться.


                      1. qw1
                        21.10.2024 17:18

                        методы — это синтаксический сахар, и на самом деле вызывается функция, которой первым параметром передаётся ссылка на объект?

                        В плюсы пока не завезли методы для простых типов, так что с этой стороны они пока ещё не объекты. Или можно писать (42).to_string(), я что-то пропустил?

                        У объектов пустого класса тоже нет состояния, и — что?

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

                        Пока всё довольно бесполезное и потенциально опасное, например, станут возможны ссылки на void, поскольку теперь можно будет разыменовать указатель на void, но эти к этим ссылкам нельзя будет обращаться

                        В чём опасность, если обращение к void - это no-op?


          1. Mingun
            21.10.2024 17:18

            Это просто следствие того, что потребовали, что каждый элемент массива должен иметь свой адрес. Чем это требование обосновано? Видимо, пока его писали, просто забыли, что элемент может иметь нулевой размер, автоматических решалок-то ограничений еще не было, никто противоречивость и не заметил


            1. eptr
              21.10.2024 17:18

              Это просто следствие того, что потребовали, что каждый элемент массива должен иметь свой адрес. Чем это требование обосновано?

              Как минимум тем, что иначе, как я написал выше, будет невозможно различать различные объекты по адресам.

              Видимо, пока его писали, просто забыли, что элемент может иметь нулевой размер, автоматических решалок-то ограничений еще не было, никто противоречивость и не заметил

              Сомневаюсь, что забыли и не заметили.

              Если вернуться к основам, то тип характеризуется набором операций, определённых над объектами этого типа, а если это ещё и тип данных, то — и размером.

              С размером — всё понятно, он равен 0.
              Какие операции были бы уместны для объектов такого типа?


              1. Mingun
                21.10.2024 17:18

                Так объект нулевого размера всегда один, что там различать?

                Операции -- ну, вот в том же Rust какие-то операции нашлись?


                1. eptr
                  21.10.2024 17:18

                  Так объект нулевого размера всегда один, что там различать?

                  Это он в одном и том же массиве — "один".

                  А, например, в такой структуре:

                  struct {
                    void v0;
                    char c;
                    void v1;
                  } s;

                  Адреса &s.v0 и &s.v1, очевидно, будут различными, и это будут разные объекты.

                  Операции -- ну, вот в том же Rust какие-то операции нашлись?

                  Rust — это Rust, а здесь — C++.

                  Так какие операции уместны для типа с нулевым размером, кроме взятия адреса и операции "запятая" (которая, кстати, и сейчас работает для выражений типа void)?

                  Какова "польза" от наличия такого типа?

                  Перевешивает ли она вред от потери возможности различать объекты по адресам?


                  1. Mingun
                    21.10.2024 17:18

                    А адреса &s и &s.v0 будут одинаковыми, но снова очевидно, что это разные объекты. И что?

                    Для начала надо бы понять, зачем нам вообще различать объекты по адресам. Что это дает? Если сами объекты неразличимы, то зачем требовать, чтобы их адреса различались?

                    Второе, зачем нам требовать, чтобы объекты нулевого размера вели себя всегда точно также, как объекты ненулевого. Ноль -- уже само по себе особое число, почему требование равнять его на других? В математике вы же не требуете, например, чтобы умножение 0 на 1 давало 1, хотя все остальные числа при умножении на 1 дают 1. Почему с размерами типов должно быть по другому?


                    1. eptr
                      21.10.2024 17:18

                      А адреса &s и &s.v0 будут одинаковыми, но снова очевидно, что это разные объекты.

                      Адреса массива и его первого элемента тоже совпадают.

                      s.v0 — подобъект объекта s, здесь нет ничего удивительного.
                      Добавьте ещё оно поле ненулевого размера перед полем v0, и тогда адреса s и s.v0 перестанут совпадать.

                      К обсуждаемому это не относится.

                      И что?

                      Речь шла о том, один ли объект нулевого размера, или их может быть несколько.

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

                      Для начала надо бы понять, зачем нам вообще различать объекты по адресам. Что это дает?

                      Например, можно защититься от присваивания самому себе.
                      Если этого — мало, можно почитать эту ветку на SO.

                      Если сами объекты неразличимы, то зачем требовать, чтобы их адреса различались?

                      Для начала ответьте на вопросы:

                      Какие операции уместны для типа с нулевым размером, кроме взятия адреса и операции "запятая"?

                      и

                      Какова "польза" от наличия такого типа?

                      А то вы обсуждаете, как должен быть введён этот тип, забыв обдумать, а — нужен ли он такой вообще?


                  1. qw1
                    21.10.2024 17:18

                    Так какие операции уместны для типа с нулевым размером

                    Пустое множество операций уместно ))

                    Какова "польза" от наличия такого типа?

                    Так вы за веткой не следите. Я начал этот холивар, чтобы унифицировать 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 как обычного типа!

                    Перевешивает ли она вред от потери возможности различать объекты по адресам?

                    Да вроде никто не отбирает эту возможность, всё что раньше работало - продолжит работать.


                    1. eptr
                      21.10.2024 17:18

                      Пустое множество операций уместно ))

                      То есть, сам по себе тип — бесполезен.

                      Так вы за веткой не следите. Я начал этот холивар, чтобы унифицировать 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 по адресам не будет.


                      1. qw1
                        21.10.2024 17:18

                        То есть, сам по себе тип — бесполезен

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

                        А для чего здесь искусственная промежуточная переменная r?

                        потому что реальный код может быть:

                           TResult r = innerFunc(args);
                           log(r);
                           return r;
                        

                        или даже

                           TResult r;
                           try {
                              r = innerFunc(args);
                           } catch (...) {
                              log(args);
                              throw;
                           }
                           return r;
                        

                        Вот — другой, универсальный вариант, работающий и для void'а:

                        Вот это полезный пример. Не знал, что такой синтаксис допустим.

                        возможности различать объекты типа void по адресам не будет.

                        "никто не отбирает эту возможность" - значит, со старым кодом, с непустыми объектами, всё будет работать как работало.


                      1. eptr
                        21.10.2024 17:18

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

                        Это и сейчас возможно.

                        потому что реальный код может быть:

                        Передать void в функцию нельзя, поэтому в таком случае не получится.

                        или даже

                           TResult r;
                           try {
                              r = innerFunc(args);
                           } catch (...) {
                              log(args);
                              throw;
                           }
                           return r;

                        А вот в этом случае — всё получится, можно упростить:

                           try {
                              return innerFunc(args);
                           } catch (...) {
                              log(args);
                              throw;
                           }

                        Вот это полезный пример. Не знал, что такой синтаксис допустим.

                        Так в статье же это описано.

                        "никто не отбирает эту возможность" - значит, со старым кодом, с непустыми объектами, всё будет работать как работало.

                        Но зато проблемы будут с новым.


                      1. qw1
                        21.10.2024 17:18

                        Передать void в функцию нельзя, поэтому в таком случае не получится.

                        А если бы void был обычным типом, для которого логгер был бы специализирован писать строку "void", то получилось бы. И не нужно было бы писать 2 варианта обёрток.


                      1. qw1
                        21.10.2024 17:18

                        Но зато проблемы будут с новым

                        Не могу придумать случай, когда нужно отличать меджу собой пустые объекты. Если задумка в том, чтобы создавать пустые объекты в куче и их адреса использовать как уникальные идентификаторы, то std::atomic<long>::fetch_add(1) справится намного эффективнее.


        1. boldape
          21.10.2024 17:18

          Я думаю эта очевидная идея не пришла в голову только лишь вам. Я не знаю, но предполагаю, что ее реализация сложнее/хуже в реалиях 70х годов чем запрет на значение. У вас есть ллвм можете сами попробовать сделать Си где войд размера, 0 но имеет значение и посмотреть сколько проблем вылезет по дороге.

          Будет отличное чтиво если ещё и статью по результатам запилите.


          1. qw1
            21.10.2024 17:18

            Бессмысленно. Мне это нужно не для теоретический изысканий, а для применения на практике. Даже если эксперимент "взлетит", C/C# уже не изменятся.


            1. boldape
              21.10.2024 17:18

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

              Возникает противоречие либо все ваши коменты в этой ветке бессмысленны либо все таки смысл создания такого языка есть, но вам этого делать не хочется. Это нормально, но тогда не удивляйтесь, что вашу идею с 0 размером войда никто серьезно не воспринимает т.к. обоснованно опасаются за проблемы которые это несёт. Вы утверждаете, что проблем нет коллективное мы вам не верим, мысленного экспиремента, даже не эксперимента, а просто обрывков рассуждений не достаточно.


              1. qw1
                21.10.2024 17:18

                Цель моей активности в этой ветке - собрать аргументы разработчиков против значений с нулевым размером. Возможно, это обогатит мои знания, если найдутся интересные кейсы. Пока нашлось только "так нельзя, потому что нельзя" и "значения, даже пустые, обязательны должны иметь разные адреса, потому что... ну, потому что".

                Хотя, вот обнаружил занятный синтаксис

                void some_func() {
                    return other_func();
                }
                

                Интересно, в формальной грамматике справа от этого return что - "значение"? Тогда void - полноценное значение (rvalue), как минимум. Или эта конструкция в грамматике прописана как исключение.
                В си постоянно обнаруживаются неожиданные синтаксические конструкции.


                1. boldape
                  21.10.2024 17:18

                  Ну например

                  char str[] = "xz";
                  void* ptr = str;
                  const void xz;
                  *ptr = xz;


                  1. qw1
                    21.10.2024 17:18

                    Что показывает этот пример?

                    По моему предложению, чтение-запись void - это no-op, поэтому

                    *ptr = xz;
                    

                    ничего не делает.


                    1. boldape
                      21.10.2024 17:18

                      В общем то сейчас ровно то же самое за исключением того что хранить значение войд нельзя. Создавать можно - (void)0, читать/писать тоже можно - дефолтовый конструктор и любые функции без пераметров. Только хранить нельзя. Почему нельзя я не знаю, но раз все остальное можно, а хранить нельзя очевидно была причина иначе бы сделали. Поэтому вам нужно сначало найти причину почему хранить войд значение нельзя, а уже имея причину можно понять почему размер 0 не поможет.

                      Наверное правило любой лвалью должно иметь свой адрес не такое уж и глупое и для чего то нужно, а не просто так. Я думаю это суть проблемы. Вероятно отсутствие различимого адреса как то сильно усложняет код ген или анализ или оптимизации или вообще жесткое требование абстрактной машины в чьи инструкции все преобразуется.


                      1. datacompboy
                        21.10.2024 17:18

                        Я так понимаю, всё ради удобства.

                        auto ptr = malloc(100);
                        ptr += 50;
                        

                        В случае нулевого void'а -- ptr не изменяется. В случае void'а считаемого в 1 байт -- ptr указывает на середину теперь.

                        Да, можно делать через касты:

                        ptr = (void*)((char *)ptr + 50);
                        

                        Но это может поломать имеющийся код. Хотя в чем проблема просто ругаться на арифметику с void'ами не только под -Wpedantic -- не представляю. Скорее всего, лень.


                      1. boldape
                        21.10.2024 17:18

                        https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0146r1.html

                        Многа букав, но там есть хороший разбор альтернатив и одна из алтернатив как раз 0 размер и описание что поламается - итераторы.

                        Папир не принят и я не смог найти что с ним не так и почему не принят.


                      1. qw1
                        21.10.2024 17:18

                        Многа букав

                        Хороший, но про 0-size там буквально 1 абзац.

                        Итераторы в std наверное можно специализировать, как итератор по std::vector<bool> ходит не по байтам, а по битам.


                      1. qw1
                        21.10.2024 17:18

                        auto ptr = malloc(100);
                        ptr += 50;
                        

                        Сейчас не работает.
                        https://godbolt.org/z/1ooondMr6
                        Опасения в том, что оно вдруг заработает?


                      1. datacompboy
                        21.10.2024 17:18

                        Работает, в gcc.


                      1. qw1
                        21.10.2024 17:18

                        Интересно, это UB или ошибка компилятора...


                      1. datacompboy
                        21.10.2024 17:18

                        https://gcc.gnu.org/onlinedocs/gcc/Pointer-Arith.html -- и оно там с доисторических времён, возможно, даже с прошлого тысячелетия... почему?


                      1. qw1
                        21.10.2024 17:18

                        тут пишут, что это всё-таки нарушение Стандарта компилятором, а не UB или IDB.


                      1. datacompboy
                        21.10.2024 17:18

                        Да, это "расширение" стандарта как они считают.

                        В коде это вот тут: 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"


      1. Gorthauer87
        21.10.2024 17:18

        Точно не знаю когда там bottom type и unit type придумали. Точно знаю они уже были такие в Haskell, но он сильно позже Си был создан.

        Но по факту, void это не полноценный unit type. В полноценном должна быть возможность создавать значение этого типа, просто оно единственное. Так что void получится где-то между unit и bottom type, вторым он тоже, очевидно, не является. Иначе бы void функции никогда бы не возвращали управление, а они фактически же возвращают то самое значение void, но его нельзя при этом присвоить переменной.

        Вот такие вот пироги.


  1. tenzink
    21.10.2024 17:18

    Всё это, конечно, занимательно. Но, почему не был сделан немедленный откат супер-фреймворка?!


    1. dalerank Автор
      21.10.2024 17:18

      Настоящие лид, не признает своих делает ошибок


  1. stepsoft
    21.10.2024 17:18

    А как же классический if constexpr? Или enable_if, или requires на сам метод test_running_time? Тогда и boid не потребовался бы. Или у этого типа есть ещё какой то смысл?


  1. datacompboy
    21.10.2024 17:18

    Так а в чем была разница в тех 50 тестах которые так и не починились сразу? Неужто не интересно?


    1. dalerank Автор
      21.10.2024 17:18

      В феврале-мае 23 в google bench внесли изменения, которые в некоторых конфигурациях тестов приводили к результату void. Но в предыдущей версии это войд ловился самим фреймворком и возвращался enum { ResultError }, а в новой void просто прокидывался дальше. Для исправления пришлось бы переписывать эти тесты под новый интерфейс, что потянуло бы изменения в зависимостях уже в тестах игры и движка, что по времени получалось немало, поэтому решили подхачить бенчмарк и не переписывать кучу кода.


      1. lazy_val
        21.10.2024 17:18

        У товарищей из гугла в github ни одного инцидента на тему void result не находится, ни в открытых, ни в закрытых

        Либо я плохо ищу, либо получается что вы одни на эту засаду налетели, либо не одни, но все остальные налетевшие тоже гордо промолчали


        1. datacompboy
          21.10.2024 17:18

          Я бы очень хотел взглянуть на пример теста, который может "приводить к результату void"...

          Ну то есть тест нормальный, но внезапно овойдивается.


  1. Serpentine
    21.10.2024 17:18

    Я не знаю историю появления типа void в языке, но было бы интересно узнать, возможно кто-нибудь в коментариях и напишет.

    Говорят, что Стивен Борн (автор Bourne Shell) еще задолго до ANSI C предложил Деннису Ритчи включить void в язык, т.к. ему не нравилось, что нельзя было создать функции, которые ничего не возвращают (до этого по умолчанию они должны были возвращать значения типа int).

    Само слово из Алгола 68, спецификацию к которому в свое время писал Борн.

    Вот ссылка на quora.com там есть два видео, где он об этом рассказывает. (возможно потребуется VPN).

    На том же сайте Стив Джонсон еще рассказал, как появился указатель на void в качестве замены char*, который возвращала malloc().


  1. Krey
    21.10.2024 17:18

    Напомните мне, в шутке про отстрел ноги речь про какой тип оружия была?


  1. 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)...);
    }
    


    1. anz
      21.10.2024 17:18

      По сути этот вариант эквивалентен SCOPE_EXIT. Автор пишет что такой вариант помог, но еще оставались некомпилируемые тесты


      1. eao197
        21.10.2024 17:18

        Показанный в статье вариант со SCOPE_EXIT -- это творение какого-то сумрачного разума, в котором invoke вынуждены вызывать дважды, причем в первом случае все равно зачем-то делают сохранения результата вызова в auto result.

        Так что нет, не вижу эквивалентности.


        1. dalerank Автор
          21.10.2024 17:18

          спасибо поправил, не заметил что хабр съел комментарии там //


  1. 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

    Профит? Нет?

    Прим.: комментарии все не осилил, может что-то не учёл.


  1. ncpuma
    21.10.2024 17:18

    А перегрузить шаблон на void нельзя было?

    Ну всмысле специализацию сделать.