#c
Вопрос:
в C существует проблема, связанная с тем, что части реализации, а именно встроенные функции и частные переменные-члены, просачиваются в файл заголовка. Встроенные функции больше не нужны при использовании LTO (за исключением функций constexpr). Существуют обходные пути для частных переменных в заголовке, например, идиома Pimpl, но у них есть свои недостатки, т. е. громоздкость в использовании и/или неидеальная производительность. На мой взгляд, в настоящее время существует простое решение этой проблемы: разрешить прямое объявление классов/структур и вручную указать их размер. У нас уже есть эта концепция в C для перечислений. Перечисление может использоваться, например, как член структуры до того, как будут известны именованные значения:
enum E : int;
struct S
{
E e;
float foo;
};
Если мы теперь расширим эту концепцию для пересылки объявлений классов/структур, мы могли бы сделать следующее (я заимствую синтаксис для битовых полей, чтобы указать размер на данный момент):
// header s.h
// forward declare SVariables and tell the compiler it is 16 bytes in size
struct SVariables : 16;
class S : private SVariables
{
public:
void foo();
};
// implementation s.cpp
struct SVariables
{
float x, y, z, w;
};
void S::foo()
{
x = 1.0f;
}
Ответственность за указание правильного или достаточно большого размера в прямом объявлении будет лежать на программистах (что также относится к перечислениям). Однако ошибки немедленно приведут к ошибке компилятора в файле реализации, когда класс/структура полностью определены, а ранее указанный размер не соответствует или слишком мал.
(Компилятор, вероятно, не сможет автоматически генерировать специальные функции-члены, например, конструктор, деструктор, конструктор копирования и т. Д., Если прямой объявленный класс/структура не является тривиальным.)
Ответ на главный вопрос, конечно, таков: потому что стандарт этого не позволяет. Поэтому я хотел бы задать следующие вопросы:
- Какие проблемы я в настоящее время не вижу?
- Рассматривалось ли это когда-нибудь?
- Если нет, стоит ли это предлагать?
(Я действительно не смог найти ничего подобного вышеупомянутой концепции на www, поэтому, пожалуйста, извините меня, если это уже где-то обсуждалось.)
Спасибо тебе, джффмичи
Комментарии:
1. как компилятор выдаст ошибку неправильного размера, если у него нет определения?
2.
enum
действительно легко переслать объявление. Это просто новый тип, определенный поверх существующего. Как вы можете знать, что он будет100
немного шире? Компиляторам разрешено добавлять отступы, поэтому трудно гарантировать размер класса.3.
struct alignas(16) SVariables{};
? Однако вам придется унаследовать его вSVariables
реализации.4. Это один из вопросов «почему этого нет в стандарте». Правильный ответ обычно звучит так: «потому что его там нет».
5. ИМХО, программисту было бы сложнее всего угадать, каким будет фактический размер. Некоторые типы имеют размеры, зависящие от реализации, даже в обычных и текущих компиляторах. В частности, указатели имеют длину 4 или 8 байт. И каждая виртуальная функция добавляет запись en в таблицу v (я знаю, что это деталь реализации, но любой вопрос о размере…). Кроме того, вы затрагиваете фундаментальную точку языка, унаследованную от C, понятие полного типа. Ваше предложение добавит новую концепцию частично полного типа. Поскольку это глубоко заложено в языке, я даже не могу представить себе последствия.
Ответ №1:
Вероятно, это связано с тем, что это достаточно легко сделать в библиотеке, но такое средство не широко используется, демонстрируя отсутствие спроса. Например:
#include <cstddef>
#include <new>
#include <utility>
template<std::size_t Size, std::size_t Align, class T>
struct fwd {
alignas(Align) std::byte buf[Size];
template<class... Args> requires (sizeof(T) <= Size amp;amp; alignof(T) <= Align amp;amp;
!std::is_same_v<fwd(std::remove_cvref_t<Args>...), fwd(fwd)>)
fwd(Argsamp;amp;... args) { new (buf) T(std::forward<Args>(args)...); }
fwd(fwdamp;amp; rhs) : fwd(std::move(rhs.get())) {}
fwd(fwd constamp; rhs) : fwd(rhs.get()) {}
fwdamp; operator=(fwdamp;amp; rhs) { get() = std::move(rhs.get()); return *this; }
fwdamp; operator=(fwd constamp; rhs) { get() = rhs.get(); return *this; }
~fwd() { get().~T(); }
Tamp; get() { return *std::launder(reinterpret_cast<T*>(buf)); }
T constamp; get() const { return *std::launder(reinterpret_cast<T const*>(buf)); }
};
Реально, если вы платите за оптимизацию барьера компилятора, который LTO не полностью устраняет, то дополнительные затраты на бесплатное распределение хранилища не будут особенно заметны и будут иметь существенное преимущество в том, что вам никогда не придется изменять размер или выравнивание (да, даже если вы планируете заранее).
Комментарии:
1.
fwd(Argamp;amp;...)
ловитfwd(fwdamp;)
.2. Существует std::aligned_storage для
buf
.3. @Jarod42 исправлено, спасибо, эти мошеннические конструкторы копирования всегда причиняют боль. Не вижу в этом смысла
std::aligned_storage
для себя.4. Вы используете эквивалентный код
std::aligned_storage
дляbuf
. Это была не ошибка, а просто «улучшение» значения типа 🙂5. У меня этого не было на радаре. Что касается Pimpl: в некоторых случаях я считаю отрицательным не только (удаление)выделения, но и дополнительное косвенное указание указателя. Ваш код избегает и того, и другого. Очевидно, что это не то, что вы использовали бы в каждом отдельном классе, но я думаю, что в некоторых случаях я адаптирую ваше решение. Большое спасибо!