• «Современный» C++: сеанс плача с причитаниями
    0
    Совершенно аналогичный код можно написать на чистом Си.
  • «Современный» C++: сеанс плача с причитаниями
    0
    Всякий раз перед возвратом (CO_YIELD) из «корутины» мы запоминаем место (номер строки), откуда осуществляем возврат. Когда функция вызывается вновь, мы возвращаемся в это место (сразу после оператора return).
    Достигается это путём обёртывания всего тела функции в switch и расстановке меток case __LINE__: во всех местах возврата, благо switch в C/C++ позволяет прыгать даже внутрь вложенных блоков (самое известное применение чему — Устройство Даффа).
  • «Современный» C++: сеанс плача с причитаниями
    +3
    (это предварительный синтаксис, потому что в стандарте C++ корутин нет):
    Зато есть старый добрый вычисляемый goto switch:
    #include <iostream>
    #include <tuple>
    
    #define CO_BEGIN \
    	{ \
    		switch (lineno) \
    		{ \
    		case 0: \
    	//
    #define CO_END }}
    #define CO_YIELD(value) \
    	do { \
    		lineno = __LINE__; \
    		return (value); \
    		case __LINE__:; \
    	} while (false) \
    	//
    
    class PytripleGenerator
    {
    public:
    	std::tuple<int, int, int> next()
    	CO_BEGIN
    		for (z = 1; ; ++z)
    			for (x = 1; x <= z; ++x)
    				for (y = x; y <= z; ++y)
    					if (x*x + y*y == z*z)
    						CO_YIELD(std::make_tuple(x, y, z));
    	CO_END
    private:
    	int lineno = 0;
    	int x = 1, y = 1, z = 1;
    };
    
    PytripleGenerator pytriples()
    {
    	return PytripleGenerator();
    }
    
    int main()
    {
    	PytripleGenerator py;
    	for (int i = 0; i < 100; ++i)
    	{
    		auto [x, y, z] = py.next();
    		std::cout << "(" << x << ", " << y << ", " << z << ")\n";
    	}
    }
  • Как не надо писать код
    0
    А в C null pointer обязательно имеет именно нулевое битовое представление?


    Нет, конечно. Я имею в виду, что использование memset для зануления всех полей структуры (равно как и calloc для выделения памяти под структуру с изначальным её занулением) — распространённый приём в Си, поэтому, если последовательность из нулевых байтов не будет хотя бы одним из возможных object representation для null pointer value, то сломается куча кода.

    А из соображений совместимости Си и C++ у этих языков должны быть одинаковые object representation для null pointer value.
  • Как не надо писать код
    +2
    Пардон — не value-initialized, а zero-initialized. Но для переменной типа «указатель на функцию» это одно и то же.
  • Как не надо писать код
    +4
    Вас просили взять «адрес существующей функции». Функция у вас здесь одна — это main, и адрес вы её не берёте (да и нельзя по Стандарту). Вы вообще никакой адрес здесь не берёте, а просто сравниваете value-initialized переменную типа «указатель на функцию» с null pointer value.

    Что-то уж совсем толсто.
  • Как не надо писать код
    +2
    Во-первых, я здесь приводил прямую цитату из Стандарта C++. Не может.

    Во-вторых, по поводу ваших ссылок. Если объявить переменную типа «указатель на функцию», то она (а не адрес какой-либо функции!), конечно, может иметь значение null pointer value.

    Только вот в этом случае она ни на какую функцию не указывает и потому вызывать её нельзя. Более того, последнее прямым текстом сказано по одной из ваших же ссылок:
    >… calling a null pointer is undefined

    Представим на секунду, что у некой функции нулевой адрес (в смысле, её адрес — null pointer value). Возьмём адрес этой функции. Попробуем её вызвать через получившийся указатель… И получим неопределённое поведение. Не находите это странным? Ваша ссылка не подтверждает, а опровергает ваш тезис.

    В описанной на reddit-е ситуации в программе неопределённое поведение. Неопределённое поведение может проявляться _как_угодно_, это в принципе ничего не может говорить о языке.

    В данном случае компилятор видит, что указатель Do может иметь лишь два возможных значения: _либо_ null pointer value, _либо_ адрес функции EraseAll. Вызов null pointer value — неопределённое поведение, поэтому компилятор, видя вызов через указатель Do, предполагает (имеет полное право, по самому определению UB), что значением указателя является всё-таки адрес функции EraseAll — и в качестве оптимизацию производит замену вызова по указателю на вызов непосредственно EraseAll — при том, что на самом деле указатель Do всегда имеет значение null pointer value.

    Это не говорит о том, что адрес функции EraseAll есть null pointer value. Это просто пример того, как компилятор честно пришёл к _ложному_ выводу по той причине, что программист нарушил контракт со своей стороны (допустил неопределённое поведение — в данном случае вызов через нулевой указатель).
  • Как не надо писать код
    0
    Потому что operator<< перегружен для const void* (к которому приводятся любые объектные типы), но не для указателей на функции (кроме функций с сигнатурами манипуляторов вроде endl или flush). Выводить адреса объектов полезнее, чем адреса функций.
  • Как не надо писать код
    +2
    Если там есть поле mutable std::vector<double> с миллиардом элементов, то самое оно.
  • Как не надо писать код
    +1
    На C/C++ точно не надо, ужасный язык. Надо на C++, или, на худой конец, на C.
  • Как не надо писать код
    +1
    Стандарт явно разрешает (а в C++17 — даже требует) компилятору устранять временные объекты (а также, в меньшей степени, именованные) путём конструирования объектов непосредственно в конечной точке цепи копирований/перемещений.
    en.cppreference.com/w/cpp/language/copy_elision
  • Как не надо писать код
    0
    Не асинхронный, а буферизованный. flush дёргается при завершении программы из деструктора глобального объекта класса std::ios_base::Init: en.cppreference.com/w/cpp/io/ios_base/Init

    У меня в Федоре 1123 строки получается. (std::endl, в отличие от просто '\n', если что, делает flush — может быть вы его написали?)
  • Как не надо писать код
    0
    endl = '\n' + flush
  • Как не надо писать код
    0
    Верно. Правда, учитывая количество любителей занулять структуры memset-ом (и соображения совместимости C++ с C) и просто неудобство подхода, на практике разработчики компиляторов на такое никогда не пойдут)

    В любом случае преобразование адреса функции к bool гарантированно возвращает true, так как оно опирается именно на null pointer value.
  • Как не надо писать код
    +2
    Что нарушает соглашение что 0 это не валидный указатель

    Это требование Стандарта, а не просто «соглашение».
    A null pointer constant can be converted to a pointer type; the result is the null pointer value of that type and is distinguishable from every other value of object pointer or function pointer type.
    (C++17 7.11 Pointer conversions [conv.ptr];
    C++11/14 4.10 Pointer conversions [conv.ptr])
    A null pointer constant can be converted to a pointer type; the result is the null pointer value of that type and is distinguishable from every other value of pointer to object or pointer to function type.
    (C++98/03 4.10 Pointer conversions [conv.ptr])
  • Как не надо писать код
    +3
    В C++17 fold-expression с операцией следования позволяет без рекурсии совершить произвольное действие над всеми элементами parameter pack-а.
    Простой (но плохой) пример:
    #include <utility>
    #include <iostream>
    
    template<typename T>
    void do_smth(T&& arg)
    {
            std::cout << arg << '\n';
    }
    
    template<typename... Types>
    void do_smth_with_all(Types&&... args)
    {
            (..., do_smth(std::forward<Types>(args)));
    }
    
    int main()
    {
            do_smth_with_all(9, 1.3, "string");
    }

    Мне пригодилась в игрушечном проекте при расчёте вклада отдельной наблюдаемой (зависящей от известного на этапе компиляции числа модельных переменных, разного для наблюдаемых разных видов) в общий градиент ошибки — выглядело это так:
    (..., (gradient[asIndex(observable.vars_[ArgsIndices])] += residual * std::get<ArgsIndices>(observable_gradient)));
    Здесь ArgsIndexes — параметр шаблона, объявлен как
    std::size_t... ArgsIndices
  • Без new: Указатели будут удалены из C++
    0
    Опциональная поддержка GC (как я понимаю, без возможности вызова деструкторов) появилась ещё в C++11 (safely-derived pointers, std::declare_reachable и т. п.).
  • Без new: Указатели будут удалены из C++
    0
    В шаблонах даже без функций есть неоднозначности между объявлениями и выражениями — как, например, в случае a::b * c или a::b<c> d, где a — параметр шаблона.
  • Без new: Указатели будут удалены из C++
    +4
    Сломается обратная совместимость, ибо new T^(...) — валидная конструкция с вполне определённым смыслом:
    struct T{};
    
    T *operator^(T *lhs, T)
    {
    	return lhs;
    }
    
    int main()
    {
    	T x;
    	T *ptr = new T^(x);
    }
    


    Синтаксис C++ и так перегружен по части всевозможного использования закорючек — в стандарте в некоторых случаях приходится явно разрешать неоднозначность интерпретации тех или иных конструкций (например, является ли T() типом функции, возвращающей значение типа T, или созданием временного объекта типа T).
  • C++ велосипедостроение для профессионалов
    +2
    К чему это написано

    К вашему вопросу «Когда это у нас std::string стал mt-safe?».

    и спащена цитата ниже?

    Цитата из стандарта C++.

    «Если вы используете потоки в реализации — вы должны сами обеспечивать их безопасность», но из этого никак не следует то, что строка mt-safe.

    Неверно. Перевожу на русский, раз такие трудности:
    «Реализации могут расшаривать их собственные внутренние объекты между потоками, если эти объекты невидимы для пользователей и защищены от гонок.»

    «Реализация» — это компилятор+стандартная библиотека, если что. В частности, реализация std::string.

    Потокобезопасность std::string (в том смысле, что два потока могут параллельно работать с двумя разными объектами std::string, пусть даже полученными из одного объекта std::string присваиванием или конструктором копии) следует из всей совокупности требований раздела «Data race avoidance», в частности:
    A C++ standard library function shall not directly or indirectly access objects (1.10) accessible by threads other than the current thread unless the objects are accessed directly or indirectly via the function’s arguments, including this.
    A C++ standard library function shall not directly or indirectly modify objects (1.10) accessible by threads other than the current thread unless the objects are accessed directly or indirectly via the function’s non-const arguments, including this.


    В рамках статьи говорилось о велосипедах, вот и говорите о них.

    Я отвечаю на ваш комментарий про якобы потоконебезопасный std::string, а не на статью.

    Вы написали два вопроса про std::string и один — про COW-строки, таким образом, словно это три вопроса на одну и ту же тему.
  • C++ велосипедостроение для профессионалов
    +3
    Когда это у нас std::string стал mt-safe?

    С тех самых пор, как появились потоки и само понятие потокобезопасности, как следствие — то есть с C++11. Все классы потокобезопасны (на базовом уровне), если не оговорено обратное; в частности, здесь применимо следующее правило:
    Implementations may share their own internal objects between threads if the objects are not visible to users and are protected against data races.
    (С++11/14, 17.6.5.9 Data race avoidance, абзац 7
    C++17, 20.5.5.9 Data race avoidance, абзац 7)

    На каком основании вы требуете mt-safe от COW-строки

    В C++ нет понятия «COW-строка», есть просто «строка». COW — это один из возможных видов реализации, про которые пользователь ничего знать не обязан, ибо инкапсуляцию никто не отменял. Связанные с COW проблемы потокобезопасности лежат исключительно на разработчиках стандартной библиотеки.
  • C++ велосипедостроение для профессионалов
    +2
    Стандартного restrict-а нет, но есть как расширение, например, в gcc (__restrict__/__restrict).

    Если -fstrict-aliasing что-то «испортил», код в любом случае надо переписывать. К тому же, он включается по умолчанию уже на -O2.
  • C++ велосипедостроение для профессионалов
    +3
    Кодировка (точнее, проблема зоопарка кодировок) беда прежде всего винды. В цивилизованном мире просто используется UTF-8.
  • C++ велосипедостроение для профессионалов
    +1
    При этом компилятор даже не поверит вам, если вы везде напишите const, потому что он не оптимизирует, основываясь на const. Если вы где-то написали const, компилятор проигнорирует это, потому что где-то в другом месте вы могли написать const_cast.


    Оптимизирует. Пример (присваивание *ptr = 9; может изменить x2, но не x1): godbolt.org/g/QPvwup С массивами получается аналогично.

    Модификация константного объекта (посредством const_cast или другими путями) — неопределённое поведение, компилятор всегда имеет право полагать, что такого не происходит.

    Другое дело, если нам передали параметр типа const T& или const T* и он ссылается на на самом деле неконстантный объект.
  • Полусинтетический организм с шестью основаниями ДНК теперь умеет размножаться
    0
    Не забывайте, что две нити ДНК имеют противоположную ориентацию. Второй цепочкой будет AATGGCGATT.
  • Почему физики всё ещё используют Fortran
    0
    В C/C++ для этого требуется следующая запись:

    int **array;
    array = malloc(nrows * sizeof(double *));

    for(i = 0; i < nrows; i++){
    array[i] = malloc(ncolumns * sizeof(double));
    }

    Для освобождения массива в Fortran

    deallocate(name_of_array)

    В C/C++ для этого

    for(i = 0; i < nrows; i++){
    free(array[i]);
    }
    free(array);


    1) Исправьте int на double.
    2) Не в C/C++, а в C. В C++ для этого есть vector, а начиная с C++14 — dynarray (в частности, не нужно явное освобождение — в C++ автоматическое управление ресурсами). А в C99 есть variable length arrays, вообще идеально решающие эту задачу (правда, в C11 ставший опциональным, и с C++ они несовместимы).
    3) Это явно не самый лучший способ выделить память под двумерный массив. Лучше выделить единым блоком — и выделение/освобождение проще, и работа будет эффективнее за счёт большей локальности данных и меньшей косвенности в обращении (правда, индексировать его будет сложнее).

    Хотя не являющиеся программистами люди действительно так и будут писать.

    Проблема в том, что const real отличается от простого real. Если функция, принимающая real, получит const real, она вернёт ошибку.


    ???

    int f(double x);
    int fc(const double x);
    ...
    double y = 5;
    const double yc = 9;
    f(y); //OK
    fc(y); //OK
    f(yc); //OK
    fc(yc); //OK
    


    Может, имелась в виду какая-нибудь передача по указателю, для которого забыли написать const? (T* к const T* прекрасно преобразуется, если что.)

    C – язык сравнительно примитивный, поэтому физики, избирающие себе C/C++, занимаются объектно-ориентированным программированием.

    Это предложение сломало мне мозг. Имелось в виду, что физики, выбирающие C/C++, в силу примитивности C смотрят в сторону C++, а он объектно-ориентированный? И жалуются потом, что им сложно? И поэтому надо выбирать фортран?

    Странная логика. Тем более что ООП — просто парадигма. Можно и на C++ писать в процедурном стиле (и не углубляться в изучение языковых средств поддержки ООП), а можно и на фортране мутить ООП, если очень надо.

    Объекты – очень громоздкие структуры по сравнению со структурами данных, предпочитаемыми физиками: массивами.


    В C++ классы могут быть неполиморфными, в силу чего их объекты физически будут представлять собой просто записи из полей (подобно derived type в том же самом фортране).

    Объекты и массивы сравнивать бессмысленно, они разные задачи решают. Но даже если совсем забить на инкапсуляцию и вообще философию ООП, то «объекты» и «массивы» — это разные способы построения одних типов данных из других, они друг с другом не конкурируют. Массив — большое или неопределённое количество однородных элементов, объект — определённый набор разных по типу и/или смыслу элементов (лишь с учётом вышесказанного; на самом деле, разумеется, эта некая целостная сущность предметной области, а не агрегат для данных).

    Если бы мы передавали его по ссылке, переданные данные не располагались бы в памяти подряд.


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

    Если первый индекс — индекс молекулы, то координаты одной молекулы будут располагаться подряд. В противном случае программисту на Си придётся вручную сформировать временный массив. Если только он явно не сделал одним из передаваемых параметров шаг (наряду с указателем на первый элемент и размером), специально для таких случаев.

  • Думаешь, ты знаешь Си?
    0
    Ваш пример не может быть в ASCII по той простой причине, что в нём есть кириллица. Которой в 7-битной ASCII не предусмотрено:)

    Он может быть в ASCII-совместимой кодировке — например, в UTF-8 (весьма вероятно, если вы под Linux-ом) или Windows-1251 (если вы под виндой).
  • Думаешь, ты знаешь Си?
    +1
    В цивилизованном мире UTF-8 — кодировка по умолчанию. Так что если вы сохранили исходник в UTF-8, то и результирующая строка в норме должна быть в UTF-8, даже в отсутствие префикса u8 (как минимум, у меня под линуксом в gcc дела обстоят именно так). Это не требуется стандартом (source character set и execution character set, в принципе, могут быть разными), но и не противоречит ему.

    Чтобы (достоверно) увидеть ошибку выхода за границы массива в вашем примере, надо знать компилятор и кодировку исходника. Без этой информации отсутствие выхода за границы более вероятно.
  • Думаешь, ты знаешь Си?
    +1
    Например вижу ошибку выхода за границу массива

    То есть вы ещё не перешли на utf-8?
  • Думаешь, ты знаешь Си?
    0
    Как это относится к теме более строго контроля типов?

    Приведу пример. Допустим, есть у нас некая функция, которая по каким-то причинам (крайне устаревший код, некомпетентность автора, потенциальное наличие заведомо нереализуемых в данной ситуации особых вариантов поведения и т. д.) принимает параметр типа char*, а не const char*, но аргумент свой, как мы точно знаем, не модифицирует:
    int f(char *str);
    
    А нам нужно вызвать её из другой функции, передав аргумент типа const char*. Приходится использовать const_cast:
    int g(const char *str, int x)
    {
        return x + f(const_cast<char*>(str));
    }
    

    Если человек на фоне недосыпа вместо этого случайно написал
    int g(const char *str, int x)
    {
        return x + f(const_cast<char*>(x));
    }
    
    то компилятор ему об этом скажет: const_cast<char*>(x) — невалидная конструкция.

    Но в языке Си мы вынуждены использовать cast notation (ничего другого нет) — там аналогичная функция
    int g(const char *str, int x)
    {
        return x + f((char*)x);
    }
    
    с точки зрения компилятора будет полностью валидной.

    В одном случае контроль типов сработал, в другом — нет.
    В любом случае, «созвездие» С++-style casts позволяет вам сделать все, что угодно (не говоря уже о том, что C-style cast никто в С++ не запрещал).

    В любом случае ремни безопасности можно не пристёгивать.

    Если у человека есть _цель_ отстрелить себе ногу, то C++-style casts ему действительно ничем не помогут. Но контроль типов — это немного про другое.
  • Думаешь, ты знаешь Си?
    0
    Таки имеют.

    5.1.1.3 Diagnostics
    A conforming implementation shall produce at least one
    diagnostic message (identified in an implementation-defined
    manner) if a preprocessing translation unit or translation
    unit contains a violation of any syntax rule or constraint...

    В сноске:
    ...Of course, an implementation is free to produce any
    number of diagnostics as long as a valid program is still
    correctly translated. It may also successfully translate
    an invalid program.

    Warning — одна из реализаций «diagnostic message». А успешная компиляция программы допускается даже если последняя некорректна.
  • Думаешь, ты знаешь Си?
    +1
    В качестве доказательства приводится тот факт, что чей-то уютненький компилерчик выдает на это «всего лишь warning».

    Понятно, что к реальности такие заявления никакого отношения не имеют.

    Как минимум, gcc (не такой уж и уютненький, вполне себе серьёзный, по строгой поддержке стандарта в том числе) по умолчанию ведёт себя именно так. Можно и const сбрасывать, и вообще совершенно разные объектные типы неявно приводить друг к другу — будет только предупреждение, а при компиляции C++-программ — ошибка.

    Diagnostic message есть — формально требование Стандарта со стороны компилятора соблюдено.
  • Думаешь, ты знаешь Си?
    +1
    Явные касты же — не по теме. Явные касты — это средства обхода контроля типов.

    Так обходить по-разному можно. (T)v значит вообще практически все проверки выключить, в то время как специализированные касты отключают лишь некоторые проверки (например, static_cast и reinterpret_cast не позволят ненароком сбросить const).
  • Думаешь, ты знаешь Си?
    +1
    вместо int, short, long и long long ввести int<«N»>где «N»-гарантированное минимальное число бит в платформозависимом типе;


    Во-первых, гарантии там и так есть (исходя из <limits.h>):
    • char, signed char, unsigned char — не меньше 8 бит;
    • short, unsigned short, int, unsigned int — не меньше 16 бит;
    • long, unsigned long — не меньше 32 бит;
    • long long, unsigned long long (since C99) — не меньше 64 бит.


    Во-вторых, начиная с C99 есть <stdint.h> с его [u]int_least<N>_t и [u]int_fast<N>_t (соответственно «самые маленькие» и «самые быстрые» типы с минимальной заданной битностью).

    Вместо int8_t, int16_t и т.д. — ввести похожую конструкцию, но с чётко фиксированным размером в битах.


    У них-то как раз фиксированные размеры — ровно столько значащих бит, сколько указано. Более того, для них гарантируется также:
    • отсутствие битов-заполнителей;
    • кодирование при помощи двоичного дополнительного кода (в то время как для остальных знаковых типов допускается любая pure binary system — как минимум прямой код и обратный код, а не только двоичный дополнительный).

    Нефиксированным остаётся лишь порядок байтов.

    Если платформозависимый размер int еще оправдан


    На практике он, как ни странно, оказался относительно платформонезависимым — 32 бита что на 32-битных, что на 64-битных платформах:) В отличие от long, который (в беззнаковом варианте), например, в ядре Linux используется чуть ли не как синоним void*.
  • Думаешь, ты знаешь Си?
    +1
    Ещё перечисления (преобразующиеся к целочисленным типам, но не обратно), особенно появившиеся в C++11 scoped enumerations (к которым и целочисленные типы не преобразуются).

    Явное приведение типов (которое в cast notation преобразует практически что угодно к чему угодно) разбито на static_cast, const_cast и reinterpret_cast (а также имеющий смысл только для C++ dynamic_cast).

    Объектно-ориентированным программированием на Си тоже занимаются, реализуя наследование вложением одних структур в другие. Отсутствие встроенных для этого средств приводит к использованию грязных хаков, со статическим контролем типов при этом всё совсем плохо. Например, в Glib Object System такие функции, как g_object_ref/g_object_unref/g_object_new принимают/возвращают просто void*, чтобы не приходилось явно приводить типы; в gtk функции создания виджетов возвращают GtkWidget* (указатель на базовый класс всех виджетов, а не на конкретный класс виджета), и т. д.
  • Думаешь, ты знаешь Си?
    0
    В ядре активно используются gcc-шные расширения, но «объектно-ориентированные» фишки реализуются вполне себе штатными средствами чистого Си. Никакого особого «Си с классами» там нет:)
  • Думаешь, ты знаешь Си?
    +1
    Добавлю ещё, что для стандартных типов тоже есть определённые гарантии (если уж мы вынуждены писать на с89), следующие из описания хедера <limits.h>:
    • char, signed char, unsigned char — не меньше 8 бит (в точности — CHAR_BIT бит), нет битов-заполнителей;
    • short, unsigned short — не меньше 16 значащих бит;
    • int, unsigned int — не меньше 16 значащих бит;
    • long, unsigned long — не меньше 32 значащих бит;
    • long long, unsigned long long — не меньше 64 значащих бит (но эти типы появились лишь в C99).
  • Думаешь, ты знаешь Си?
    0
    То есть sizeof(int) == 4 еще не означает что int равен четырем байтам


    Тут два момента.

    С одной стороны, означает:
    The sizeof operator yields the size (in bytes) of its operand, which may be an expression or the parenthesized name of a type.


    С другой стороны, стандарт Си описывает всё в терминах абстрактной машины, не привязываясь ни к каким свойствам реальных машин. В Си своя модель памяти, которая не обязана сколько-нибудь явно соответствовать организации памяти той реальной среды, в которой программа будет выполняться (собственно, и не соответствует: компиляторы одни переменные помещают в регистры, другие полностью выкидывают, переупорядочивают операции чтения/записи и т. д.). Поэтому и понятие «байт» в стандарте Си, строго говоря, не обязано совпадать с понятием байта в «железе».
  • Думаешь, ты знаешь Си?
    0
    Третий вопрос полностью о темных углах. Начиная с того, что ни переполнения integer


    Переполнение возникнет лишь если результат умножения не помещается в int.

    Преобразование между целочисленными типами данных (в данном случае от int к char) — немного другая статья. Если результирующий тип — беззнаковый, то гарантируется модулярная арифметика по модулю 2^<число бит>; в противном случае результат будет implementation-defined (в поздних стандартах на этот случай появилась фраза "...or an implementation-defined signal is raised", но это всё равно не undefined behavior).

    Более того, размер типа char в битах не определен. Существовали платформы где он был по 6 бит (помните триграфы?) и существуют платформы где все пять целочисленных типов по 32 бита.


    Стандарт требует, чтобы CHAR_BIT был не меньше 8 бит.
  • Спросите Итана №69: убегает ли от нас Вселенная?
    0
    Те же самые круги можно и на шаре нарисовать:)

    Проще это только тем, что не вызывает вопросов про четвёртое измерение.

    > чуть проще понять почему есть «горизонт» — после определенного количества таких кругов скорость «прироста пространства» становится равной скорости света, а потом становится больше чем скорость света

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