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

Комментарии 34

const std::vector<int>& __range = Foo().Get() ;. В итоге получаем висящую ссылку.

Вроде, тут нет проблем, объект будет удалён, когда закончится scope для __range.

Невнимательно посмотрел, действительно баг.

Просто поделюсь хорошей новостью по поводу #embed, наконец-то в Visual C++ убрали ограничение на длину строки, правда пока все обновятся... https://learn.microsoft.com/en-us/cpp/cpp/string-and-character-literals-cpp?view=msvc-170#size-of-string-literals

In versions of Visual Studio before Visual Studio 2022 version 17.0, the maximum length of a string literal is 65,535 bytes. This limit applies to both narrow string literals and wide string literals. In Visual Studio 2022 version 17.0 and later, this restriction is lifted and string length is limited by available resources.

И на всякий случай https://learn.microsoft.com/en-us/cpp/c-language/maximum-string-length?view=msvc-170

А это точно эффективный код для преобразования строки в enum?

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

Чего еще давно не хватает - типизированного указателя аналога void* для полиморфных объектов. какой-нибудь std::object* чтобы к нему можно было привести указатель на любой полиморфный объект и в дальнейшем безопасно сделать dynamic_cast

Встроенный в язык способ преобразования строк в enum ожидается от статической рефлексии. Предложения по ней рассматриваются с повышенным приоритетом, все надёются увидеть рефлексию в C++26 (но лично я сомневаюсь, что успеем).

А std::any вам не подойдёт вместо std::object* ? Работает даже и не с полиморфными объектами

any_cast приводит только к тому типу который хранится в any, dynamic_cast к любому в дереве наследования. Обычный POD тип и полиморфный в С++ - фундаментально разные вещи. Почему до сих нет указателя на полиморфный тип непонятно. Получается как в том анекдоте про вовочку ж..а есть а слова нет.

А для каких именно случаев вам нужен такой указатель? Приведите пожалуйста примеры

В программах с GUI происходит регулярное стирание и востановление типа. В обработчиках событий и функциях типа GetSelectedObject. std::any в этом случае не пройдет т.к. в дальнейшем "обезличенные" типы опрашиваются на наследование от определенных типов/интерфейсов.

Типичный пример такого стирания типов:
function_ref<R(Args...)>;
и логика близкая к string_view вместо string - не нужно создавать временный объект, без каких либо аллокаций происходит эффективное стирание типа и допустим вы имеете в хедере сигнатуру
void foo(function_ref<int(float)>);
Вы не создаёте кучу инстанцирований функции foo, можете вынести реализацию в .cpp, к тому же это хорошо инлайнится(так как нет аллокаций)
Другой пример - std::make_format_args, которое стирает типы. Это можно представить как
print(fmt_str, any_printable::const_ref...);
да и вообще любые использования Interface* / & это как раз то самое стирание.

Опять же, допустим есть значение типа X и его нужно передать в функцию foo(Interface&);, даже в случае с виртуальными функциями можно передать обычную ссылку(если X поддерживает Interface), не создавая ничего лишнего. И хочется иметь такую возможность для типов не поддерживающих явно какие-то виртуальные интерфейсы


Ну и библиотеки делающие это есть... https://github.com/kelbon/AnyAny

Звучит интересно! Готовы помочь с написанием предложения, но готовьтесь к тому что будет тяжко принять такое в стандарт...

В виде библиотеки принять в стандарт будет попроще :)

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

Пока я пытаюсь добавить поддержку в кланг(минимальную хотя бы), просто в качестве демонстрации

С ним ней не всё так хорошо. У нас например в проекте есть здоровый enum, который представляет ID на какую-либо строку. Этот enum генерируется другим инструментом вместе с локализациями. И тут две проблемы:

  1. ID там достигают значений порядка 10000 (количество реальных значений в enum меньше, остальные берутся другими способами). И вот, перелопатить это невнятное количество в compile time (а именно это и делает эта библиотека) - серьёзное увеличение времени компиляции, а ещё скорее всего по количеству итерации свалится.

  2. Генератор этого enum по ряду причин не может нам дать максимальный ID. Поэтому не понятно, как задавать пределы (библиотека их требует).

А есть какие-то планы на управление зависимостями? Или считается, что это не часть языка, поэтому такое не рассматривают?

Работа идёт, и она в приоритете для C++26.

Сейчас комитет отошёл от идеи стандартизировать какой-то имеющийся пакетный менеджер или систему сборки. Вместо этого сосредоточились на создании стандартного языка описания зависимостей и сборки. Чтобы с таким файлом описания вы могли собрать проект и с помощью cmake, и с помощью basel, и с помощью conan...

Безопасный range-based for

В качестве компромисса, для того, чтобы обезопасить range-based for, можно использовать редко применяемую возможность указать вид ссылочности this:

class SomeData {
 public:
  // ...
  const std::vector<int>& Get() const & { return data_; }
  const std::vector<int> Get() const && { return data_; }
  std::vector<int> Get() && { return std::move(data_); }
 private:
  std::vector<int> data_;
};

Компромисс, потому что теперь "не висит", но — за счёт накладных расходов на copy&move.

Что интересно: если бы поле data_ не было бы private, то в следующем коде висящей ссылки не возникло бы:

  for (int v: Foo().data_) {
    std::cout << v << ',';
  }

Жаль, что метод не позволяет "пробросить" свойство поля, если им инициализирована ссылка, продлевать жизнь всего объекта на время жизни ссылки.

#embed это для тех, кто не научился в линковку?

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

А ещё результат #embed доступен в constexpr, так что можно включать разлтчные таблицы констант и делать compile time вычисления над ними.

у кого платформа не предоставляет

В каком смысле «платформа»? Как такового механизма прилинковывания произвольных файлов и не должно существовать. Линкер должен давать возможность скормить ему на вход произвольное число произвольных объектных файлов — не важно какими путями на свет порожденных. И линкеры базово эту возможность имеют. Остается только найти или написать тривиальный инструмент, который генерирует объектный файл на базе произвольного файла.

Ну, с compile-time вычислениями хоть какой-то смысл прослеживается. Но опять же, правильный подход — это написать утилиту, которая получив на вход бинарный файл с данными на выходе выплюнет текстовое представление этих данных, которое потом будет просто #includeиться. И никакое #embed не нужно будет, потому что только подобная кастомная утилита знает, как интерпретировать входной бинарник. Может там массив байтов? А может массив слов? А может массив массив 64-битных целых чисел? А может массив 64-битных чисел с плавающей точкой? А какой endianness? А может там массив структур?

Из объектного файла пропадет информация о размере массива.

Утилиты такие есть. Например ресурсы в Qt.

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

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

Все перечисленное это все равно массив байт. И нет, утилита ничего не должна знать что там хранится и в каком порядке байт. Ей это не нужно. Это нужно функциям, работающим с этими данными.

Из объектного файла пропадет информация о размере массива.

Какой смысл вы вкладываете в словосочетание «из объектного файла пропадает»? Лежит на диске объектный файл, там эта информация есть, и вдруг она внезапно пропадает оттуда — объектный файл по прежнему лежит там, где лежал, но информации о размере там уже нет. Вряд ли вы что-то подобное имеете в виду.

Тогда давайте разберёмся, что?

Она вообще не попадает в объектный файл?
Или в объектных файлах вообще нет места подобной информации, потому что формат объектных файлов не предусматривает хранение такой информации?
Или она там есть, но её никто не принимает во внимание?

Вообще, какие отношения вас связывают с форматами объектных файлах ELF и COFF? Знаете эти форматы «на зубок», или просто имеете примерное представление, что объектные файлы хранят и зачем это нужно? Надеюсь, что вы всё-таки достаточно хорошо представляете, что там.

Так вот, если говорить об объектных файлах формата ELF, сам по себе формат ELF предусматривает хранение размера ELF-символа. В структуре Elf32_Sym/Elf64_Sym есть поле st_size именно для этого.

typedef struct {
        Elf32_Word      st_name;
        Elf32_Addr      st_value;
        Elf32_Word      st_size;  // <------------
        unsigned char   st_info;
        unsigned char   st_other;
        Elf32_Half      st_shndx;
} Elf32_Sym;

typedef struct {
        Elf64_Word      st_name;
        unsigned char   st_info;
        unsigned char   st_other;
        Elf64_Half      st_shndx;
        Elf64_Addr      st_value;
        Elf64_Xword     st_size;      // <--------------
} Elf64_Sym;

То есть как минимум, теоретически возможность сохранять информацию о размере в ELF-формате объектных файлов предусмотрена. И она не только существует, а ещё и используется на практике: компиляторы в это поле записывают актуальный размер сущности, к которой относится символ в таблице символов.

Например GCC:

// demo.c
const char uni[1] = {42};
const char bi[2] = {'A', 'z'};
const char tri[3] = {6,6,6};
const char quadro[4] = "foo";
const char penta[5] = {9,8};
gcc -c demo.c
objdump -t demo.o
demo.o:     file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 l    df *ABS*  0000000000000000 demo.c
0000000000000000 l    d  .text  0000000000000000 .text
0000000000000000 l    d  .data  0000000000000000 .data
0000000000000000 l    d  .bss   0000000000000000 .bss
0000000000000000 l    d  .rodata        0000000000000000 .rodata
0000000000000000 l    d  .note.GNU-stack        0000000000000000 .note.GNU-stack
0000000000000000 l    d  .comment       0000000000000000 .comment
0000000000000000 g     O .rodata        0000000000000001 uni
0000000000000001 g     O .rodata        0000000000000002 bi
0000000000000003 g     O .rodata        0000000000000003 tri
0000000000000006 g     O .rodata        0000000000000004 quadro
000000000000000a g     O .rodata        0000000000000005 penta

Предпоследняя колонка — это именно оно. Размер.

Так что, как видите, GCC размер массива в объектный файл кладёт.
Остаётся только взять его оттуда при линковке, но для это требуется, чтобы на уровне формата ELF поддерживался тип релоков, который бы позволил при резолвинге подставлять не адрес сущности, а размер сущности. Предусмотрен ли в формате ELF такой тип релоков? Хороший вопрос. Типы релоков специфичны для каждой аппаратной платформы. Так вот, для x86, x86_64 aka amd64 и других популярных аппаратных архитектур такой тип релоков есть.

Например, для x86 это R_386_SIZE32.

Так что на уровне линковки линкер вполне способен разрулить ситуацию, когда в первом объектном файле есть ссылка на размер массива, который живёт в другом объектном файле, а во-втором объектном файле есть этот самый массив. Линкер возмёт размер сущности из таблицы символов второго файла и подставить это значение (точнее скорректирует на величину этого значения) в заданное место в куске машинного кода или данных из первого объектного файла.

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

И вот только в этом месте начинаются проблемы.
Это не объектные файлы сами по себе не способны хранить размер массивов.
Это не линкер сам по себе не способен подставлять размер массива там, где попросят (а не адрес массива, как просят обычно)
Это лишь на уровне компилятора C/C++ сделано так, что компилятор by-design отказывается компилировать sizeof, применённый extern int foo[], выбрасывая ошибку, вместо того, чтобы сгенерировать объектный файл с соответствующей ссылкой на размер сущности, которую подставил бы линкер, ориентируясь на информацию из другого объектного файла.

Но это ELF. А что с COFF?

В COFF в таблице символов у символа нет поля, аналогичного st_size из ELF. Но есть кое-что другое.

Во-первых, у COFF-символов есть тип символа и класс символа, а значит и два соответствующих поля, а кроме того, у символов в таблице символов есть обязательная часть постоянного формата и размера и опциональная auxilliary-часть, формат и размер которой зависит от типа/класса COFF-символа. То есть записи символов с COFF не имеют постоянной длины (но размер записей так или иначе кратен некоей гранулярности, равной размеру обязательной части символа — это к слову). Так вот, форматом COFF предусмотрен такой тип как DT_ARY, который предполагает наличие вслед за обязательной частью aux-записи, в которой хранится размер массива.

Вот первый механизм, посредством которого в объектных файлах COFF можно было бы хранить информацию о размере массива. Но этот существующие компиляторы, например компилятор Microsoft, этот путь не использует. Типы символов как таковые не используются. Потому что нет таковой надобности. Используя sizeof на сущности из других объектных файлов мы ссылаться не можем, а раз так, то зачем сохранять в COFF-OBJ-файлы размеры массивов? И вот они и не сохраняются. К тому же нет соответствующих типов релоков, которые позволили бы на этот размер массива сослаться из другого места — а это уже серьёзно.

Но во-вторых, есть принципиально иной механизм. Причём концепция, позволяющая этому механизму быть, присутствует как в ELF, так и в COFF.

По сути, и в ELF, и в COFF объектный файл представляет собой три кита: заголовок файла, таблицу секций (с атрибутами каждой секции и указанием, где в объектном фале лежат данные для этой секции) и таблицу символов (с атрибутами символов и ссылкой на секцию, которой принадлежит этот символ).

Выделенное жирным — ключевое. В ELF-символах это поле st_shndx, а в COFF-символах это поле SectionNumber (по номенклатуре Microsoft) или **n_scnum`` (в рамках оригинальной номенклатуры, берущей начало, видимо, из System V).

// Microsoft:
typedef struct _IMAGE_SYMBOL {
    union {
        BYTE    ShortName[8];
        struct {
            DWORD   Short;     // if 0, use LongName
            DWORD   Long;      // offset into string table
        } Name;
        PBYTE   LongName[2];
    } N;
    DWORD   Value;
    SHORT   SectionNumber;
    WORD    Type;
    BYTE    StorageClass;
    BYTE    NumberOfAuxSymbols;
} IMAGE_SYMBOL;
typedef IMAGE_SYMBOL UNALIGNED *PIMAGE_SYMBOL;

// System V:
{
	char		n_name[8];	/* Symbol Name */
	long		n_value;	/* Value of Symbol */
	short		n_scnum;	/* Section Number */
	unsigned short	n_type;		/* Symbol Type */
	char		n_sclass;	/* Storage Class */
	char		n_numaux;	/* Auxiliary Count */
}

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

И в COFF, и в ELF предусмотрено несколько специальных значений этого поля, и одно из них: это ABS. Если в норме поле «значение» хранит смещение сущности относительно начала секции, индекс которой приведён в поле «индекс секции», то ABS это означает, что символ не «живёт» в какой-то секции, а поле «значение» означает не смещение в секции, а абсолютное значение, то есть, грубо говоря, числовую константу. В COFF за это отвечает специальная константа IMAGE_SYM_ABSOLUTE, в ELF — константа N_ABS.(и там, и там она равна минус единице).

Это именно то, что позволяет сохранить в COFF размеры сущностей, в частности, массивов. И вот тут как раз соответствующий тип релоков, чтобы просто взять и подставить в нужное место абсолютное значение абсолютного COFF-символа в COFF предусмотрен для всех аппаратных архитектур. Например, для x86 это IMAGE_REL_I386_ABSOLUTE.

И тут остаётся только один момент: как положить в объектный файл COFF одновременно информацию и о смещении массива в рамках секции и о размере массива, если оба таких символа будут иметь одинаковое имя, а имя — это единственный способ ссылаться на сущность из других объектных файлов?

А очень просто: как компилятор С++ помещает в объектный файл (будь то COFF или ELF) информацию символы для vftable, vbtable класса, или же символы, соответствующие разным вариантам перегруженной функции или перегруженного оператора? Он декорирует имена. Причём каждый компилятор использует свою схему декорирования. Даже компилятор C (не C++) от MS использует нечто вроде декларирования имён, когда раздаёт имена COFF-символам. Так вот, для хранения информации о массива foo, если бы стандарт предусматривал применение sizeof к extern-массивам, размер которых не очевиден из объявления, то достаточно было бы придумать схему декорирования для присвоения имени COFF-символу, несущему абсолютное значение размера.

Так что после всего это хочу сказать:
Объектные файлы прекрасно могут хранить размеры массивов — хранение такой информации прямо заложено в ELF и прекрасно доступно в COFF. Способ ссылаться из одного объектного файла на размер сущности из другого тоже предусмотрен напрямую в ELF, и как частный случай ссылок на абсолютные константные значения в COFF. Касательно COFF я также дополнительно проверил: «а поддерживает ли линкер этот механизм ссылки на ABS-символы, который на практике вроде бы как не используется?» — прекрасно поддерживате, как оказалось. Не верите: сформируйте два COFF .obj-файла вручную (в хекс-редакторе, например) и попробуйте слинковать их MS-овским линкером. Все прекрасно линкуется.

Информация из объектных файлах не пропадает: она там есть, если её туда положил компилятор, и она оттуда берётся и используется (линкером), если линкер попросят её взять и использовать (а линкер об этом просит компилятор).

Так что дело исключительно в компиляторе.

И вот, вместо того, чтобы закрыть пробел вокруг уже давно существующей части языка — вокруг применение sizeof к extern-массивам неуказанного в декларации размера (при том, что нижележащий инструментарий в виде объектных файлах и линкера со своей стороны поддерживает такую возможность), что делает комитет и горе-предлагальщики?

Вместо логичного решения чуть-чуть довести до ума существующую конструкцию языка они привносят в язык ИДИОТСКИЙ КОСТЫЛЬ в виде новой директивы препроцессора #embed.

А главное (ВЫ ТОЛЬКО ВДУМАЙТЕСЬ!), что #embed ВООБЩЕ НИКАК не решает обозначенную вами проблему. Вот вы говорите, что убрать большой прединициализированный массив в отдельный объектный файл это так себе решение, потому что хоть обращаться к нему можно из каждого файла-исходника, размер (с помощью sizeof) получить не удастся ни откуда.

А как чёртова #embed решает эту проблему? Абсолютно никак. Если вы объявите огромный массив табличных значений в foo.cpp, загнав в него полгига табличных значений с помощью #embed, то из bar.cpp, baaz.cpp, test.cpp, core.cpp, main.cpp вы по прежнему не можете получить её размер (с помощью sizeof, без промежуточной функции из того же объектного файла, что и сам массив)!

Хотя примерно даже понятно, как из этого будут выкручиваться CPP-программисты. Поскольку с введением COMDAT в объектные файлы C++-программистам разрешили объявлять одно и то же в разных компилируемых файлах (лишь бы на этапе линковки оно оказалось одинаковым во всех файлах), C++-программисты будут помещать объявление полгигабайтного массива не в .h-файл, который будут подключать везде, где только можно.

В итоге, при компиляции полгигабайтный массив будет попадать в каждый объектный файл, а потом уже линкеру придётся отбросить дубликаты. Гениальное стратегическое решение, ничего не скажешь...

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

Она ОБЯЗАНА знать, для того, чтобы в массиве оказались правильные значения, если только вы не согласны с мыслью, что костыль #embed настолько костыль, что годится только для инициализации байтовых массивов, но не массивов int16_t, int32_t, int64_t.

Комментатор выше написал, что основная ценность этого подхода с #embed по сравнению с прилинковыванием заранее заготовленной и упакованной в .obj/.o-файл таблицей это возможность применить оптимизацию на основе compile-time расчётов.

Допустим, у нас есть какой-то алгоритм, который полагается на таблицу простых чисел, и чтобы не вычислять последовательность простых чисел, оно вшита в программу:

uint16_t PrimeNumbers[] = {
2,	3,	5,	7,	11,	13,	17,	19,	23,	29,	31,	37,
41,	43,	47,	53,	59,	61,	67,	71,	73,	79,	83,	89,
97,	101,	103,	107,	109,	113,	127,	131,	137,	139,	149,	151,
157,	163,	167,	173,	179,	181,	191,	193,	197,	199,	211,	223,
227,	229,	233,	239,	241,	251,	257,	263,	269,	271,	277,	281,
283,	293,	307,	311,	313,	317,	331,	337,	347,	349,	353,	359,
367,	373,	379,	383,	389,	397,	401,	409,	419,	421,	431,	433,
439,	443,	449,	457,	461,	463,	467,	479,	487,	491,	499,	503,
509,	521,	523,	541,	547,	557,	563,	569,	571,	577,	587,	593,
599,	601,	607,	613,	617,	619,	631,	641,	643,	647,	653,	659,
661,	673,	677,	683,	691,	701,	709,	719,	727,	733,	739,	743,
751,	757,	761,	769,	773,	787,	797,	809,	811,	821,	823,	827,
829,	839,	853,	857,	859,	863,	877,	881,	883,	887,	907,	911,
919,	929,	937,	941,	947,	953,	967,	971,	977,	983,	991,	997,
/*   ... ... ... */
9739, 9743, 9749, 9767, 9769, 9781, 9787, 9791, 9803, 9811, 9817, 9829,
9833, 9839, 9851, 9857, 9859, 9871, 9883, 9887, 9901, 9907, 9923, 9929,
9931, 9941, 9949, 9967, 9973
};

В таком виде у вас в коде есть одно место, которое является авторитетным источником сведений о простых числах.

Программисту не нужно заботиться о том, как число 9973 представлено в памяти. Компилятор берёт эту заботу на себя.

Если этот код компилируют под little-endian-архитектуру, оно будет представлено как F5 B6. Если под big-endian-архитектуру — то как B6 F5.

И вот программист проникся идеей использовать #embed, ему осточертело держать таблицу простых чисел в коде, потому что ему нужны простые числа не до 10 000, а до 10 миллонов (или миллиардов). И он, потирая радостно ладошки, пишет утилиту, которая посчитает и запишет миллионы простых чисел в бинарный файл, а потом делает #embed этого бинарного файла в свой исходный код.

Вопрос номер один: он сходу идёт нафиг, потому что #embed предназначена только для инициализации байтовых массивов?

Вопрос номер два: если не только байтовых, то ему нужно один бинарный файл на случай компиляции под LE-архитектуры, и другой на случай компиляции под BE-архитектуры, переключаться между которыми нужно при помощи #if...#else...#endif?

Вопрос номер три: если эта идиотская директива #embed будет иметь дополнительные спецификаторы, которые уточнят препроцессору, что нужно поменять порядок байтов, то в каком именно порядке байтов таблица волшебных значений (простых чисел — в нашем случае) должна храниться на диске, в репозитори? Почему это должен быть именно little-endian или big-endian?

Вопрос номер три (б): а если речь идёт о хранении в таблице отрицательных значений, принимая во внимание, что в разных аппаратных архитектурах помимо разного порядка байтов (big endian vs. little endian) может практиковаться разная схема представления отрицательных чисел (дополнительный код vs. прямой код), то открывается ещё одна неопределённость? Теперь если я хочу задать большой массив 16-битных чисел с помощью бинарного файла, у меня есть целых 4 гипотетических способа того, как в этом бинарном файле может представлено число –2: как FE FF, как FF FE, как 80 02, как 02 80.

Вопрос номер четыре: а как быть, если мы пишем переносимый код, который могут компилировать под разные архитектуры, причём такие, что в одной целевой архитектуре тип int будет иметь размер 4 байта, а в другой — 2 байта (допустим, это какая-то 16-битная аппаратная архитектура). Как должен выглядеть бинарный файл, которым с помощью элемента #embed инициализируется массив int-ов?

Вопрос номер 5:
А что, если я хочу с помощью бинарного файла инициализировать массив структур, например массив 3D-векторов struct{float x; float y; float z;}? Допустим, моей программе нужно иметь огромный массив, содержащий координаты всех известных науке звёзд, и эта информация у меня есть в виде бинарного файла? А если поля в структуре гетерогенны? А если они содержат вложенные структуры? А если разные архитектуры предполагают разную ширину полей и разное выравнивание? Какую ширину должны иметь поля в бинарном файле и какие должны выдерживаться выравнивани, чтобы в массиве оказались верные значение, а не какой-то съехавший мусор? А если в массиве будет указатель?

Это нужно функциям, работающим с этими данными.

Ну да, да, верю. Я уже выше привёл доводы, но ещё раз, с другой стороны: я пишу прошивку для стиральной машинки, и объявляю массив sndDone, в котором содержатся PCM-кодированный семпл с фразой «Стирка закончена». Каждый элемент массива — это отсчёт, который будет выставлен на ЦАП.

Раньше у меня этот звук был закодирован текстовым инициализатором ( `= {-3000, -122, 0, 7956, 29744, ... }), а теперь я решил делать #embed на базе WAV-файла, от которого отказывается WAV-заголовок. Почему-то теперь, чтобы код не превратился в тыкву, функция, проигрывающая PCM-кодированный семпл, которая раньше была написана просто с расчётом на то, что она просто принимает массив 16-битных чисел, и никаких нюансов с порядком байтов и быть не может, теперь должна заботиться о том, а совпадает ли используемый ею (и архитектурой в целом) порядок байтов с порядком байтов, который использовался в бинарном файле, который использовался при компиляции.

Короче, это такой жуткий костыль, что мне кажется тут обсужать нечего.

То что ваш комментарий обильно пролайкали, да и в целом, то что большинство народа поддерживает то, что на протяжении последних лет делают с С++, вызывает у меня только ужас и желание охарактеризовать всё это одной фразой:

Вот такой фразой

сгенерировать объектный файл с соответствующей ссылкой на размер сущности, которую подставил бы линкер

А шаблоны по sizeof тоже линкер инстанциировать будет?


Причем то что предлагаете вы, элементарно релизуется дополнительным extern const size_t рядом с extern int foo[].

Не хотите это оформить в отдельную статью?

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

Глядя на баланс лайков между моими комментариями, и комментариями оппонентов, можно представить себе мировоззрение апологетов modern-C++ — усомниться в правильности пути, выбранного комитетом, значит устроить себе кармическое самоубийство в рамках Хабра.

Как статья-мнение, что в C++ хотят затащить дебильную фичу? Как статья, разбирающая внутренности объектных файлов и внутреннюю кухню процесса линковки?

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

Это же не сериализатор, а просто удобное создание массива из файла.

выплюнет текстовое представление этих данных, которое потом будет просто #includeиться

Не работает, потому что

Finally, Microsoft has an ABI problem with its maximum string literal size that cannot be solved using string literals or anything treated like string literals, as the LLVM thread and the thread from Claire Xen make clear. It has also frustrated both C an C++ programmers alike, despite their best efforts. It was so frustrating that even extended-C-and-C++-compilers, like Circle, solve this problem with custom directives.

Каким вообще боком тут ABI? И вообще,я не понимаю каким образом эта цитата относится к описанному. Причём здесь строковые литералы? Когда я писал «выплюнет текстовое представление», я не имел в виду строковый литерал, я имел в виду то, что называется aggregate initializer list.

Я только что скомпилировал исходник, где объявлен и инициализирован элементами глобальный массив размером 20 Мб! 20 мегабайт, Карл! Это очень много для какой-то таблицы значений, которая должна быть известна на этапе компиляции чтобы что-то там значительно оптимизировать.

Больше не получается, потому что я использую компилятор MSVC 1998-го года выпуска, и он генерирует ошибку, что внутренняя куча, которая используется для внутренних нужд компилятора, достигла своего лимита. Потому что создателям компилятора в 1998-ом году не пришло бы и в голову, что каким-то идиотам в 2023 понадобится инициализировать массив размером в несколько десятков Мб на уровне исходного кода сишной программы.

Но давайте разберёмся: если бы #embed был 25 лет назад, #embed позволил бы внедрить в компилируемый модуль массив размером в полгига, если оставить в силе то же ограничение на размер внутренней служебной кучи компилятора? Значит проблема-то не в отсутствии директивы #embed, а в лимите размера кучи?

Значит можно решить проблему. просто соответствующим образом увеличив лимит на размер кучи без добавления #embed в язык?

Причём тут тогда ABI? По сути эти люди тащат в язык новую директиву препроцессора, потому что понимают, что Microsoft бросится имплементировать новый стандарт и им придётся так или иначе убрать этот лимит на размер кучи или применить принципиально иную архитектуру для хранения этих данных? А может просто попросить их подвинуть лимит кучи?

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

А можно стандартизировать #embed и всем будет удобно из коробки.

Весь процесс стандартизации направлен на удобство использлвания - можно обходиться без std::string, constexpr, auto, for, switch, std::map, std::vector... Можно писать эти классы самому, обходиться без автоматизации работы со стороны компилятора. Но будет не удобно

А про aggregate initializer list на 2 абзаца выше:

Creating a brace-delimited list of numbers in C comes with baggage in the form of how numbers and lists are formatted. C’s preprocessor and the forcing of tokenization also forces an unavoidable cost to lexer and parser handling of values.

Therefore, using arrays with specific initialized values of any significant size becomes borderline impossible. One would think this old problem would be work-around-able in a succinct manner. Given how old this desire is (that comp.std.c thread is not even the oldest recorded feature request), proper solutions would have arisen. Unfortunately, that could not be farther from the truth. Even the compilers themselves suffer build time and memory usage degradation, as contributors to the LLVM compiler ran the gamut of the biggest problems that motivate this proposal in a matter of a week or two earlier this very year. Luke is not alone in his frustrations: developers all over suffer from the inability to include binary in their program quickly and perform exceptional gymnastics to get around the compiler’s inability to handle these cases.

Со ссылками: https://groups.google.com/g/comp.std.c/c/zWFEXDvyTwM; https://lists.llvm.org/pipermail/llvm-dev/2020-January/138225.html; https://twitter.com/oe1cxw/status/1008361214018244608.

И ещё:

The numbers here are not reassuring that compiler developers can reduce the memory and compilation time burdens with regard to large initializer lists. Furthermore, privately owned compilers and other static analysis tools perform almost exponentially worse here, taking vastly more memory and thrashing CPUs to 100% for several minutes (to sometimes several hours if e.g. the Swap is engaged due to lack of main memory). Every compiler must always consume a certain amount of memory in a relationship directly linear to the number of tokens produced.

То есть "подвинуть лимит" позволит скомпилировать, но очень медленно и с гигабайтами памяти на каждый мегабайт исходных данных (у Clang лучше: всего сотни мегабайт). Это уже после десятилетий попыток улучшить ситуацию. Если вы знаете, как сильно оптимизировать обработку больших инициализаторов хотя бы в GCC, Clang и MSVC, вашему решению очень многие обрадуются.

А вот представление тех же данных строковыми литералами отлично и быстро работает в GCC и Clang, но в MSVC невозможно из-за ABI.

ну это уже давно исправлено

versions of Visual Studio before Visual Studio 2022 version 17.0, the maximum length of a string literal is 65,535 bytes. This limit applies to both narrow string literals and wide string literals. In Visual Studio 2022 version 17.0 and later, this restriction is lifted and string length is limited by available resources.

//  Before  
std::vector days{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
int idx = 0; for(const auto & d : days) {  
 print("{} {} \n", idx, d);   
 idx++; 
}
// After 
#include <ranges>
std::vector days{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; 
for(const auto & [index, value] :   
  std::views::enumerate(days)) {    
  print("{} {} \n", index, value); 
}   


Проблема idx всегда раздражала, что нужно описывать, так как for range не предоставляет из коробки уже объявленный idx, тратится две линии на декларирование idx вне блока и его инкрементирование и это каждый раз надо писать. Вместо этого мы получаем std::views::enumerate, мало того что это имеет длинное название, так еще и описывать раскрытие в [index, value]. Я недоволен, требуется еще больше писать кода, это так же содержит больше слов, больше чтения кода. Значит добавить новую семантику в виде for range это ок, но добавить новую семантику idx - нет? Я понимаю что новый вид конструкции будет дёргать глаз, но если это при написании потребует 1 слов, 2 символа, разве это было бы комфортно?
Что-то похожее на это:

for (const auto &d : days => idx) { ... }

получаем std::views::enumerate, мало того что это имеет длинное название

Почему бы не объявить using-альяс на весь проект, если эта конструкция в нём часто встречается?


Я недоволен, требуется еще больше писать кода, это так же содержит больше слов, больше чтения кода.

Больше символов, но меньше элементов. Вместо объявления переменной, её инициализации и инкремента только объявление structured binding и вызов enumerate. Опять же, с коротким альясом на enumerate символов тоже будет меньше (да даже с простым using std::views::enumerate;).


Значит добавить новую семантику в виде for range это ок, но добавить новую семантику idx — нет?

Дело в том, что ranged for — универсальный механизм, а предлагаемый вами дополнительный синтаксис поверх него решает только одну частную проблему (пускай и сравнительно частую). Добавление enumerate решает его не хуже, при том, что это всего лишь одна из множества библиотечных функций, а не специальный синтаксис.


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


Ну и для демонстрации проблемы немного разовью вашу идею:


std::vector days{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; 
std::vector days_ru{"Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"}; 
// Текущий стандарт. Слишком многословно
using namespace std::views;
for (const auto &[idx, day, day_ru] : zip(days, days_ru) | reverse | enumerate) { ... }
// Вот так гораздо лучше:
for (const auto &day, &day_ru ^: days, days_ru => idx) { ... }
// Объявление двух переменных в левой части и перечисление через запятую в правой имеет особое значение итерации по двум коллекциям сразу.
// ^ перед двоеточием означает проход в обратном направлении.
// => в соответствии с вашим предложением.
// P.S. Осталось придумать что делать, если мы хотим, чтобы индексы тоже в обратном направлении шли. Предлагаю внести в стандарт ^=>
Зарегистрируйтесь на Хабре , чтобы оставить комментарий