Идея с NVI и запросом интерфейса через приведение указателей на несовместимые типы мне понятна.
Что using применяется для "обезжиривания" интерфейса — тоже понятно.
Чисто субъективно, мне не нравятся операторы приведения, я бы сделал функции типа asSerializable() или querySerializable()
для явного запроса.
Но суть моих сомнений в другом.
Все хорошо получается, когда рассматриваем один класс CBar и жирный интерфейс ISuperBar.
Попробуем распространить идею на два класса CCat и СDog.
Оба должны быть поддерживать (каким-то способом) ISerializable и полиморфно обрабатываться в foo(ISerializable*).
Здравствуйте, qaz77, Вы писали:
Q>Чисто субъективно, мне не нравятся операторы приведения, я бы сделал функции типа asSerializable() или querySerializable() Q>для явного запроса.
Ну это вопрос вкуса, если не вкуса, то можно сделать где-то в каком-то IUnknown шаблонный NVI метод
а в ISuperBar определить нешаблонные виртуальные перегрузки GetAs.
Кроме того, можно определить шаблонный метод GetAs без параметров
template<typename I> I* GetAs() {
I* i = 0;
return this->GetAs( i );
}
template<typename I> const I* GetAs() const {
const I* i = 0;
return this->GetAs( i );
}
Тогда всё будет каститься единообразно.
Q>Но суть моих сомнений в другом. Q>Все хорошо получается, когда рассматриваем один класс CBar и жирный интерфейс ISuperBar. Q>Попробуем распространить идею на два класса CCat и СDog. Q>Оба должны быть поддерживать (каким-то способом) ISerializable и полиморфно обрабатываться в foo(ISerializable*).
Q>Итак, как нам объявить ISerializable?
Ты же говорил, что у тебя только одна популярная версия MDT?
Q>Выход я вижу только в объявлении интерфейса собакота.,
Нет, выход в том, что бы найти популярную пачку интерфейсов, скажем IGentlemansSet, которая покрывает тот MDT, которых у тебя много
И выводишь из IGentlemansSet все нужные интерфейсы, а остальные оставляешь как есть.
В IGentlemansSet нет никакого смысла, это просто хак, что бы склеить указатели на таблицу виртуальных функций популярных интерфейсов.
Это ПОДРОБНОСТЬ РЕАЛИЗАЦИИ, семантика объекта состоит в том, что он ISerializable реализует и ещё что-то, а не какие-то другие хитрости.
Q>А в общем случае — интерфейса "всего что только есть".
Это тоже можно, но, скорее всего не особо выгодно, так как раздует все таблицы виртуальных функций. Я бы затолкал в IGentlemansSet только ПОПУЛЯРНЫЕ интерфейсы (те, vtbl_ptr которых имеет много экземпляров)
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Здравствуйте, Erop, Вы писали:
E>Ты же говорил, что у тебя только одна популярная версия MDT?
Я в начале говорил, что много объектов одного класса, реализующего ISerializable и проч.
В этом смысле — да, одна популярная версия.
Далее в ветке упоминалось, что есть 100+ других классов, которые также реализуют ISerializable.
Т.е. одна из баз (по крайней мере) не ассоциируется исключительно с многочисленным классом.
Поэтому хочется, по возможности, ограничить оптимизацию так, чтобы остальные классы затрагивались по минимуму.
Еще многочисленный MDT реализует пачку интерфейсов, которые используются только внутри библиотеки,
используют типы данных, которых нет в публичных заголовках.
Чтобы сделать супер базу для MDT, надо будет тащить все эти детали реализации в публичные заголовки,
а этого очень не хочется.
E>Нет, выход в том, что бы найти популярную пачку интерфейсов, скажем IGentlemansSet, которая покрывает тот MDT, которых у тебя много E>И выводишь из IGentlemansSet все нужные интерфейсы, а остальные оставляешь как есть.
По смыслу, я так и сделал.
Широко используемые интерфейсы, типа ISerializable, я объединил в такой IGentlemansSet, в паре классов добавил недостающую реализацию.
Далее вывел по цепочке из IGentlemansSet сначала клиентские интерфейсы многочисленного CBar, а затем, во внутренних заголовках,
интерфейсы уровня реализации.
Здравствуйте, qaz77, Вы писали:
Q>Далее в ветке упоминалось, что есть 100+ других классов, которые также реализуют ISerializable. Q>Т.е. одна из баз (по крайней мере) не ассоциируется исключительно с многочисленным классом.
Во-первых, если тебя это смущает, то ты можешь не включать ISerializable в IGentlemansSet...
Включи остальные 9 интерфейсов?
Во-вторых, то, что ISerializable защищённо выводится из IGentlemansSet — это просто оптимизация. Подробность реализации.
Хуже то, что QueryInterface надо иначе делать, но можно это дело унифицировать, если надо.
Q>Поэтому хочется, по возможности, ограничить оптимизацию так, чтобы остальные классы затрагивались по минимуму.
Ну будет часть интерфейсов защищённо выведена из IGentlemansSet. Ну и что?
Q>Еще многочисленный MDT реализует пачку интерфейсов, которые используются только внутри библиотеки, Q>используют типы данных, которых нет в публичных заголовках. Q>Чтобы сделать супер базу для MDT, надо будет тащить все эти детали реализации в публичные заголовки, Q>а этого очень не хочется.
Можно вывести из IGentlemansSet IPrivateGentlemansSet и тайные интерфейсы получать таким же образом из него.
Q>По смыслу, я так и сделал. Q>Широко используемые интерфейсы, типа ISerializable, я объединил в такой IGentlemansSet, в паре классов добавил недостающую реализацию. Q>Далее вывел по цепочке из IGentlemansSet сначала клиентские интерфейсы многочисленного CBar, а затем, во внутренних заголовках, Q>интерфейсы уровня реализации.
Я понимаю, что ты сделал, но, IMHO, так как ты сделал, хуже, так как интерфейсы из IGentlemansSet больше портятся, так как них появляются унаследованные левые методы. Можно, например, нечаянно, вызвать не то.
Q>В public/common.h
Я понимаю. как ты сделал. Я тебе это и предлагал, как альтернативу.
Просто мне вариант, когда доступны только нужные методы, а ненужные не доступны, нравится больше.
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Здравствуйте, Erop, Вы писали: E>Я понимаю, что ты сделал, но, IMHO, так как ты сделал, хуже, так как интерфейсы из IGentlemansSet больше портятся, так как них появляются унаследованные левые методы. Можно, например, нечаянно, вызвать не то.
Портятся больше, это да, не спорю.
Зато клиентский код вообще не пришлось менять.
С оператором приведения к IBarN* еще есть проблема.
У меня повсеместно работа с интерфейсами через ссылки происходит, а не через указатели.
Т.е. в клиентском коде пришлось бы массово звездочки расставлять, там где сейчас неявное преобразование.
Здравствуйте, qaz77, Вы писали:
Q>С оператором приведения к IBarN* еще есть проблема. Q>У меня повсеместно работа с интерфейсами через ссылки происходит, а не через указатели. Q>Т.е. в клиентском коде пришлось бы массово звездочки расставлять, там где сейчас неявное преобразование.
Можно операторы приведения к ссылке на интерфейс сделать, а не к указателю.
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Здравствуйте, qaz77, Вы писали:
Q>Пользователи моей софтины умудрились так размножить мелкие объекты, что довели бедняжку до bad_alloc (32 битная арх.). Q>Соответственно встала задача оптимизации потребления памяти.
Извините, что влезаю в чатик не с момента его зарождения — что-то меня тогда отвлекло...
Вот какую идею могу предложить. Вариация на тему NVI.
Если у нас туча долгоживущих мелких объектов (наличие у которых лишних служебных полей стало критично), то мы можем сделать следующее.
Пусть каждый объект умеет отдавать прокси для известных интерфейсов. То есть, у него метаинтерфейс — фактически, dynamic_cast рукодельный
struct IFoo;
struct IBar;
struct IBuz;
struct IObject {
virtual proxy<IFoo> as_foo() = 0; // proxy<> - на вкус и цвет.virtual proxy<IBar> as_bar() = 0;
virtual proxy<IBuz> as_buz() = 0;
// ну и какие-то совсем уж общие полезняшки, присущие всем объектамvirtual ~IObject() {}
virtual const char* classname() const = 0;
.....
};
// пример реализации проксиtemplate<class I> using proxy = shared_ptr<I>; // неэффективно по скорости - тройная косвенность, но как proof of concept сойдётtemplate<class T> struct impl_proxy_IFoo : IFoo {
shared_ptr<T> obj_;
impl_proxy_IFoo(shared_ptr<T> obj) : obj_(obj) {}
void foo1() override { return obj_->foo1(); } // где у T эти функции даже не обязательно виртуальныеvoid foo2() override { return obj_->foo2(); } // и даже необязательно совпадающие по именам (хотя это было бы проще)
};
template<class Final> struct impl_IObject { // один большой CRTP, или россыпью миксинов, или вообще тупо на макросах в каждом классе...
shared_ptr<IFoo> as_foo() override {
static_if( support_foo<Final>::value )
return make_shared<impl_proxy_IFoo<Final>>(static_cast<Final*>(this)->shared_from_this());
else
return shared_ptr<IFoo>();
}
. . .
};
// использование
shared_ptr<IObject> x = get_some_object_from_big_cloud();
shared_ptr<IFoo> f = x->as_foo();
f->foo1();
f->foo2();
Тут идея в том, что объектов много, а рукояток к ним в каждый конкретный момент времени — мало. Поэтому мы можем раскошелиться.
Причём рукоятки-прокси могут быть даже вообще кортежами функций. (Это ещё более лютый proof of concept, просто чтоб показать идею).
struct IFoo {
function<void()> foo1(); // или какой-то более легковесный контейнер для замыкания известной природы: T* ->* (T::*)(blablabla)
function<void()> foo2();
};
template<class I> using proxy<I> = I;
.....
IFoo as_foo() override {
return {
[this](){this->foo1();},
[this](){this->foo2();},
};
}
.....
Здравствуйте, Кодт, Вы писали: К>Вот какую идею могу предложить. Вариация на тему NVI.
shared_ptr<IFoo> f = x->as_foo();
Основная проблема с таким подходом, что в клиентском коде надо менять неявные преобразования на явные вызовы as_foo() и т.п.
Такой пример:
struct IBar: ISerializable,
IClonable,
...
{
};
В клиентском коде:
IBar& bar = findSomeBar(key);
saveToDisk("bar.dat", bar); // тип второго параметра: const ISerializable&
Предлагаемое решение потребует следующей переделки клиентского кода наподобие:
IBar& bar = findSomeBar(key);
shared_ptr<ISerializable> s = bar.as_serializable();
assert(s);
saveToDisk("bar.dat", *s);
Я не написал об этом явно, но мои многочисленные интерфейсы, выставляемые клиенту, не управляют временем жизни объектов.
У всех таких интерфейсов protected non-virtual деструктор.
Объекты живут в большом DOM'е и его внутренняя логика определяет время жизни.
Поэтому в proxy типа shared_ptr нет необходимости.
Без прокси решение вырождается в то, что предлагал Erop:
Т.е. отдачу сырого указателя + возможность неявного преобразования.
Если бы у меня функции типа saveToDisk принимали указатели, а не ссылки, то был бы удобный вариант.
Q>Основная проблема с таким подходом, что в клиентском коде надо менять неявные преобразования на явные вызовы as_foo() и т.п.
Можно всё обвешать пользовательскими кастами и проксями с семантикой ссылки.
Я писал эти dynamic_cast'ы явно, чтобы их видно было.
Q>Если бы у меня функции типа saveToDisk принимали указатели, а не ссылки, то был бы удобный вариант.
Q>Для ссылки — исключение кидать, как в dynamic_cast?
А у тебя сейчас как сделано? Вот есть у меня ISerializable&, я хочу получить из него IClonable& как получить?
И как узнать, можно или нельзя получать?
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Здравствуйте, Erop, Вы писали: E>А у тебя сейчас как сделано? Вот есть у меня ISerializable&, я хочу получить из него IClonable& как получить? E>И как узнать, можно или нельзя получать?
Получать IClonable& из ISerializable& задача не стоит.
Т.е. у меня нет логики QueryInterface во время выполнения.
Все касты неявные и во время компиляции.
struct IBar: ISerializable,
IClonable,
...
{
};
struct IBaz: IClonable,
IComparable,
...
{
};
IBar& getRandomBar();
IBaz& getRandomBaz();
void saveSomethingToDisk(const char* filename, ISerializable& s);
int main()
{
IBar& bar = getRandomBar();
saveSomethingToDisk("bar.dat", bar);
IBaz& baz = getRandomBaz();
saveSomethingToDisk("baz.dat", baz); // << compile time error, IBaz не поддерживает ISerializablereturn 0;
}
К>// вот наш клиентский код!
К>void doit(IFoo::Ref foo) {
К> foo.f();
К> foo.g();
К> IBar::Ref bar = (IObject&)foo; // поскольку IFoo и IBar независимы, делаем up-down-каст
К> bar.b();
К> bar.c();
К>}
К>
Т.е. сигнатуру клиентской функции c doit(IFoo& foo) надо поменять на doit(IFoo::Ref foo).
Клиентского кода много и не хотелось бы его трогать вообще.
И не стоит задача кастов между независимыми IFoo и IBar.
Только неявные касты от производного к базе с проверкой во время компиляции.
Т.к. Something не реализует напрямую IFoo и IBar, то приходится создавать временные объекты Thunk
с реализацией виртуальных функций. И мы получаем оверхед во время выполнения, где его раньше не было.
Что мне нравится в данном подходе, то что нет жирного интерфейса.
В IObject собраны только фабрики прокси.
IFoo и IBar по сравнению с оригинальными интерфейсами добавилось только приведение к IObject&.
Для многочисленных классов, которые не оптимизируем по кол-ву vptr, как реализовывать operator IObject&?
Т.е. такой например:
struct OldBig: IFoo
{
// реализация IFoovirtual void f() override;
virtual void g() override;
// теперь ещеvirtual operator IObject&() override { /*запретить бы его вызов в compile time*/ }
};
Q>Клиентского кода много и не хотелось бы его трогать вообще.
Ну, какое-то количество кода придётся потрогать в любом случае.
Q>И не стоит задача кастов между независимыми IFoo и IBar. Q>Только неявные касты от производного к базе с проверкой во время компиляции.
Это я сделал в подарок, практически нахаляву. Если не нужно, то просто отломи каст к IObject& у частных интерфейсов.
Q>Т.к. Something не реализует напрямую IFoo и IBar, то приходится создавать временные объекты Thunk Q>с реализацией виртуальных функций. И мы получаем оверхед во время выполнения, где его раньше не было.
Да, это компромисс пространство-время. Либо у нас маленькие объекты с медленным доступом (платим косвенностью), либо большие (платим указателями) с быстрым.
Q>Для многочисленных классов, которые не оптимизируем по кол-ву vptr, как реализовывать operator IObject&? Q>Т.е. такой например: Q>
Q>struct OldBig: IFoo
Q>{
Q> // реализация IFoo
Q> virtual void f() override;
Q> virtual void g() override;
Q> // теперь еще
Q> virtual operator IObject&() override { /*запретить бы его вызов в compile time*/ }
Q>};
Q>
В конце концов, можно все интерфейсы унаследовать от IObject, и тогда проблема отпадёт автоматически!
-----
Кстати, ещё один вариант что-то надумался.
1. Послушаем совета — сделаем мега-интерфейс IEverything (унаследованный от всех частных — и с тучей vptr'ов).
2. Сделаем у объекта фабрику, отдающую прокси этого типа. Почти так же, как IFoo::Ref::Thunk, только нам теперь не нужен Ref.
3. Будет ли эта фабрика виртуальным членом классов объектов предметной области, или извне нахлобучиваться, скажем, в функциях поиска — это обсуждабельно
Здравствуйте, Кодт, Вы писали: К>Ну, какое-то количество кода придётся потрогать в любом случае.
Клиентский код вообще не трогал.
К>Да, это компромисс пространство-время. Либо у нас маленькие объекты с медленным доступом (платим косвенностью), либо большие (платим указателями) с быстрым.
Ничем не пожертвовал. Выиграл память, но не проиграл скорость.
Может даже копеечку выиграл, т.к. при нескольких vptr каст добавляет смещение базы, а при одном — ноль.
Компромисс в другом. Размер объекта или внутренняя стройность дизайна.
Пришлось пожертвовать логикой при выведении независимых интерфейсов друг из друга.
Появился артефакт в виде возможности неявного приведения IBar к IFoo, чего раньше не было.
Старый клиентский код это не ломает, но при написании нового может озадачить.
Все же я считаю, что свойства ощущаемые потребителем, такие как выделенная память и скорость,
важнее внутренней кухни, с которой имеют дело только программисты.
тут, правда, есть подводный камень, который состоит в том, что определения типов аргументов и результатов методов из всех интерфейсов джентльменского набора придётся включить или предопределить, хотя бы. Но может это и не создаст много зависимостей.
В любом случае это не дороже наследования левых интерфейсов друг от друга.
И да, напоминаю, что типичный хедер оптимизированного интерфейса будет выглядеть теперь так:
С точки зрения пользователя библы ничего вообще не поменяется.
С точки зрения того, кто реализует неоптимизированные интерфейсы, очень мало
И только в тех классах, которые играют в оптимизацию, что-то поменяется. Но они все твои, как я понял, и их немного?
Я вижу три проблемы:
1) Нелегальный down cast. IMHO эта проблема носит чисто формальный характер. В этом смысле такое решение чуть хуже решения с выведением всех интерфейсов IGentlemansSet друг из друга.
2) В IGentlemansSet появляется лишняя связность кода. Но тут с вариантом с выведением паритет, или, даже, чуть лучше, так как типы нужные в ISerializable, в GentlemansSet.h можно только объявить, а давать им определение только в Serializable.h
3) Нет CT проверки, что в MDT реализованы все нужные методы, так как мы не можем теперь методы IGentlemansSet сделать pure virtual. Тут с "наследственной" схемой тоже паритет, но тут можно немного побороться.
Можно так реструктурировать это код, что в зависимости от ключа компиляции/макроса будет собираться обычная или оптимизированная версия, и все проверки делать в обычной.
таки прикручиваются CT проверки реализации нужных методов!
Напоминаю, что проблема у нас в том, что в оптимизированном варианте нет проверки что все нужные методы во всех MDT реализованы.
Тут подход с фейковыми интерфейсами сразу в плюсе, потому, что позволяет оставить проверки хотя бы для того клиентского кода, который в оптимизации не нуждается, а просто поддерживает ISerializable.
Но если пойти на то, что бы проверка делалась при сборке специальной неоптимизированной сборки, то всё получается вообще хорошо, кроме того, что в оптимизированном коде есть какое-то количество макросни.
Да, обрати внимание, что как-то меняется только тот код, который нуждается в оптимизации. И твой код, который в оптимизации не нуждается, и все реализации (это FreqUsed.inc, UsualUsed.h, UsualUsed.inc Test.cpp) переехал из версии "было" БЕЗ ПРАВОК
Ну и левых связностей не возникло. Все наследования из IGentlemansSet приватные, явно он нигде не фигурирует и т. д
Думаю, что на той же идее специальной тестовой сборки без оптимизации, но с проверками, можно и более шаблонное решение сделать, что бы вместо
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Здравствуйте, Erop, Вы писали:
E>Я вижу три проблемы: E>1) Нелегальный down cast. IMHO эта проблема носит чисто формальный характер. В этом смысле такое решение чуть хуже решения с выведением всех интерфейсов IGentlemansSet друг из друга. E>2) В IGentlemansSet появляется лишняя связность кода. Но тут с вариантом с выведением паритет, или, даже, чуть лучше, так как типы нужные в ISerializable, в GentlemansSet.h можно только объявить, а давать им определение только в Serializable.h E>3) Нет CT проверки, что в MDT реализованы все нужные методы, так как мы не можем теперь методы IGentlemansSet сделать pure virtual. Тут с "наследственной" схемой тоже паритет, но тут можно немного побороться. E>Можно так реструктурировать это код, что в зависимости от ключа компиляции/макроса будет собираться обычная или оптимизированная версия, и все проверки делать в обычной.
E>таки прикручиваются CT проверки реализации нужных методов!
Получилось довольно проработанное решение.
Только с двумя версиями сборки — громоздко.
Здравствуйте, qaz77, Вы писали:
Q>Только с двумя версиями сборки — громоздко.
Это всего лишь скрипт билдовый.
Текущее твоё решение вообще не проверяет, что нужные функции определены вроде как.
Или у тебя есть упорядочение интерфейсов, которое позволяет всегда выводиться только из актуальных?
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Здравствуйте, Erop, Вы писали: E>Текущее твоё решение вообще не проверяет, что нужные функции определены вроде как. E>Или у тебя есть упорядочение интерфейсов, которое позволяет всегда выводиться только из актуальных?
У меня сейчас нет фейковых реализаций функций с ассертами внутри.
В интерфейсах честные виртуальные функции = 0.
То, что прилетело нового из IGentlemanSet, проще было до-реализовать в классах. Благо, там совсем немного добавилось.
Я понимаю, что для общего случая это не пройдет.
Согласен, что твой вариант более универсальный.
Здравствуйте, qaz77, Вы писали:
Q>Я понимаю, что для общего случая это не пройдет. Q>Согласен, что твой вариант более универсальный.
Он ещё лучше защищает пользователя от неправильных кастов к незапланированным интерфейсам
И не затрагивает всех, кто хочет реализовать неоптимизированный интерфейс
Но я согласен, что на месте виднее, что лучше.
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском