Есть интерфейс, с какими-то виртуальными методами. Есть какие-то реализации этого интерфейса.
Есть отдельная реализация, которая хранит в себе указатели на несколько реализаций того же интерфейса, и каждый вызов метода через эту мульти реализацию должен вызывать этот метод через все хранящиеся указатели на другие реализации.
При этом, если метод что-то возвращает, то за результат берём вызов через первый хранящийся указатель на интерфейс, а остальное игнорируем
Как-то так
#pragma once
#include <iostream>
#include <memory>
struct IDelegationTest
{
virtual bool getter() = 0;
virtual bool setter(bool b) = 0;
virtual void jobImpl(bool b, int i) = 0;
virtual void jobImpl(bool b, int i, std::string s) = 0; // !!! А что будет с этой перегрузкой?virtual int jobImpl2(bool b, int i) = 0;
};
struct DelegationTestImpl : public IDelegationTest
{
int n = 0;
virtual bool getter() override
{
std::cout << "DelegationTestImpl::getter(), n: " << n << "\n";
return true;
}
virtual bool setter(bool b) override
{
std::cout << "DelegationTestImpl::setter(bool b), b: " << b << ", n: " << n << "\n";
return true;
}
virtual void jobImpl(bool b, int i) override
{
std::cout << "DelegationTestImpl::jobImpl(bool b, int i), b: " << b << ", i: " << i << ", n: " << n << "\n";
}
virtual int jobImpl2(bool b, int i) override
{
std::cout << "DelegationTestImpl::jobImpl2(bool b, int i), b: " << b << ", i: " << i << ", n: " << n << "\n";
return n;
}
};
struct DelegatorImpl : public IDelegationTest
{
std::vector< std::shared_ptr<IDelegationTest> > impls;
template <typename Return, typename... Args>
Return delegateCall( Return(IDelegationTest::*func)(Args...), Args...)
{
// !!! Тут assert на тему impls не должен быть пустым
std::vector<std::shared_ptr>::const_iterator it = impls.begin();
// !!! Или assert тут по it!=impls.end();
// Получаем результатauto res = it->func(Args...); ++it;
for(; it!=impls.end(); ++it)
{
// Вызываем метод для всех экземпляров имплементаций
it->func(Args...); ++it;
}
return res;
}
// template <typename... Args>
void delegateCall( void(IDelegationTest::*func)(Args...), Args...)
{
std::vector<std::shared_ptr>::const_iterator it = impls.begin();
for(; it!=impls.end(); ++it)
{
// Вызываем метод для всех экземпляров имплементаций
it->func(Args...); ++it;
}
}
// !!! Тут ещё проблема, а что будет с перегруженными виртуальными методами?virtual bool getter() override
{
return delegateCall(getter);
}
virtual bool setter(bool b)
{
return delegateCall(setter, b);
}
virtual void jobImpl(bool b, int i)
{
delegateCall(jobImpl, b, i);
}
virtual void jobImpl2(bool b, int i)
{
return delegateCall(jobImpl2, b, i);
}
};
inline
std::shared_ptr<IDelegationTest> makeDelegator()
{
DelegatorImpl impl;
impl.impls.emplace_back(std::make_shared<DelegationTestImpl>(1));
impl.impls.emplace_back(std::make_shared<DelegationTestImpl>(2));
// !!! Тут надо создать экземпляр DelegatorImpl, но вернуть надо std::make_shared<IDelegationTest>return std::make_shared<IDelegationTest>(impl);
}
int main()
{
std::shared_ptr<IDelegationTest> pDelegator = makeDelegator();
pDelegator->getter();
pDelegator->setter(true);
pDelegator->jobImpl(false, 3);
pDelegator->jobImpl2(true, 5);
}
Здравствуйте, Marty, Вы писали:
M>При этом, если метод что-то возвращает, то за результат берём вызов через первый хранящийся указатель на интерфейс, а остальное игнорируем
А вопрос-то в чём?
Пока не реализуешь все абстрактные методы класс не создать.
если есть перегрузки, придётся указывать сигнатуру метода.
если хочется создавать копию, опишите это явно.
или создавайте динамически и добавляйте уже туда реализации.
Здравствуйте, kov_serg, Вы писали:
M>>При этом, если метод что-то возвращает, то за результат берём вызов через первый хранящийся указатель на интерфейс, а остальное игнорируем _>А вопрос-то в чём?
Ну, я с вариадиками на "Вы", не очень было понятно. Плюс редко приходится пользоваться указателями на функции, тем более на виртуальные методы, в купе с shared_ptr. Куча мелких деталей, в которых я не очень ориентируюсь за редкостью использования.
_>Пока не реализуешь все абстрактные методы класс не создать.
Ну, это-то ясно
_>если есть перегрузки, придётся указывать сигнатуру метода.
Да, видимо так, хотя хотелось бы без сигнатуры
_>если хочется создавать копию, опишите это явно. _>или создавайте динамически и добавляйте уже туда реализации.
Здравствуйте, Marty, Вы писали:
M>Здравствуйте!
M>Есть интерфейс, с какими-то виртуальными методами. Есть какие-то реализации этого интерфейса.
M>Есть отдельная реализация, которая хранит в себе указатели на несколько реализаций того же интерфейса, и каждый вызов метода через эту мульти реализацию должен вызывать этот метод через все хранящиеся указатели на другие реализации.
M>При этом, если метод что-то возвращает, то за результат берём вызов через первый хранящийся указатель на интерфейс, а остальное игнорируем
Ну, вот как-то так. Без явного указания прототипа с перегрузками, как ты и хотел. Единственная небольшая шероховатость — первый обработчик всегда вызывается последним (потому, что именно его результат возвращается). Но эта мелочь легко устраняется добавлением еще одного метода делегирования (аналогичного doDelegate):
Здравствуйте, rg45, Вы писали:
R>Ну, вот как-то так. Без явного указания прототипа с перегрузками, как ты и хотел. Единственная небольшая шероховатость — первый обработчик всегда вызывается последним (потому, что именно его результат возвращается). Но эта мелочь легко устраняется добавлением еще одного метода делегирования (аналогичного doDelegate):
Очень интересно, но ничего не понятно
Без концептом никак, я правильно понимаю?
Ладно, я пожалуй лучше поживу пока с указанием прототипа для перегрузок
Здравствуйте, Marty, Вы писали:
M>Без концептом никак, я правильно понимаю?
Да вообще без проблем, здесь они только создают типовую безопасность, но принципиальной роли не играют, просто удаляются и все. Обрати внимание, следующий пример скомпилирован в режиме C++14.
Здравствуйте, Marty, Вы писали:
R>>Единственная небольшая шероховатость — первый обработчик всегда вызывается последним (потому, что именно его результат возвращается). Но эта мелочь легко устраняется добавлением еще одного метода делегирования (аналогичного doDelegate):
Я выше упоминал о некрасивости, что первый обработчик вызыается последним. На самом деле у этой проблемы есть более элегантное решение, чем добаление еще одной перегрузки метода doDelegate. Исправить последовательность вызовов можно, использовав идиому RAII (см. класс AtExit и его использование в doDelegate). Следующий пример уже работает в точном соответствии с требованиями и в правильной последовательности, при этом все делегирование выполняется одним общим методом (doDelegate):
Здравствуйте, rg45, Вы писали:
R>>>Единственная небольшая шероховатость — первый обработчик всегда вызывается последним (потому, что именно его результат возвращается). Но эта мелочь легко устраняется добавлением еще одного метода делегирования (аналогичного doDelegate):
R>Я выше упоминал о некрасивости, что первый обработчик вызыается последним. На самом деле у этой проблемы есть более элегантное решение, чем добаление еще одной перегрузки метода doDelegate. Исправить последовательность вызовов можно, использовав идиому RAII (см. класс AtExit и его использование в doDelegate). Следующий пример уже работает в точном соответствии с требованиями и в правильной последовательности, при этом все делегирование выполняется одним общим методом (doDelegate):
Не очень понятно, зачем ты разделил вектор на m_head и вектор m_tail?
Ради этого цикла?
Здравствуйте, Marty, Вы писали:
M>Не очень понятно, зачем ты разделил вектор на m_head и вектор m_tail?
Это убивает сразу нескольких зайцев. Во-первых, не нужно проверять и делать специальную обработку для пустого множества обработчиков. Во-вторых, для итерирования по коллеции я могу использовать более аккуратный цикл range for вместо циклов с использованием итераторов или счетчиков цикла. Ну и в-третьих, так лучше видно в коде, что делегатор достает именно из этого элемента. Семантика этого элемента немного отличается от остальных, поэтому лучше его сразу обособить, так код становится проще и яснее.
Ты можешь попробовать, ради эксперимента, слить этот элемент с общей коллекцией. Потом посмотришь и сравнишь. Не забудь при этом сравнить реализации с точки зрения безопасности и обработки ошибок. Только лучше бери сразу последнюю версию: http://coliru.stacked-crooked.com/a/d66ee722f822d4fc.
Здравствуйте, rg45, Вы писали:
R>Это убивает сразу нескольких зайцев. Во-первых, не нужно проверять и делать специальную обработку для пустого множества обработчиков. Во-вторых, для итерирования по коллеции я могу использовать более аккуратный цикл range for вместо циклов с использованием итераторов или счетчиков цикла. Ну и в-третьих, так лучше видно в коде, что делегатор достает именно из этого элемента. Семантика этого элемента немного отличается от остальных, поэтому лучше его сразу обособить, так код становится проще и яснее.
Здравствуйте, Marty, Вы писали:
M>А ещё не разжуёшь вот это: M>
M>for (auto&& impl : m_tail)
M>
Ну impl — это имя переменной цикла, которая, в данном случае, представляет собой ссылку на элемент вектора m_tail. Двойной амперсанд здесь используется как универсальный вариант доступа к контейнерам с различной константностью — если контейнер константный то ссылка на элемент будет константной, а если контейнер неконстантный, то и ссылка будет неконстантной. Хотя в данном случае это и не имеет особого значения, я мог бы вместо auto&& написать const auto& или просто auto и тоже все работало бы, просто по привычке использовал наиболее обобщенную запись. Описанный механизм известен под названием Forwarding references.
А вообще такая форма цикла for — это и есть как раз тот самый range-for, о котором я умоминал выше, и это является аналогом следующей записи:
for (auto it = m_tail.begin(); it != m_tail.end(); ++it)
{
auto&& impl = *it;
// . . .
}
А в C++03 полгого аналога вообще не существовало, поскольку там всегда нужно было указывать явно, какой тип итератора ты используешь — константный, или неконстантный.
template <typename Return, typename... Args>
Return delegateCall( Return(IDelegationTest::*func)(Args...), Args... args)
{
if constexpr ( ! std::is_same_v<Return,void>)
{
// !!! Тут assert на тему impls не должен быть пустым
......
Здравствуйте, rg45, Вы писали:
R>Я выше упоминал о некрасивости, что первый обработчик вызыается последним. На самом деле у этой проблемы есть более элегантное решение, чем добаление еще одной перегрузки метода doDelegate. Исправить последовательность вызовов можно, использовав идиому RAII (см. класс AtExit и его использование в doDelegate). Следующий пример уже работает в точном соответствии с требованиями и в правильной последовательности, при этом все делегирование выполняется одним общим методом (doDelegate):
R>http://coliru.stacked-crooked.com/a/d66ee722f822d4fc
А теперь, легким движением руки, переходим от динамического полиморфизма к статическому (угу, с концептами):
Можно ли как-то обойтись без std::function() ?
Я так понимаю в нём накладные расходы на дополнительный virtual-call, хотя может зависеть от реализации std, но всё же
Что про вызов в RAII — тут можно получить exception из деструктора ...
Здравствуйте, Sm0ke, Вы писали:
S>Я так понимаю для TS нужна именно виртуальность. Чтобы была возможность определить impl класс в run-time
Все может быть, конечно, я не спорю. В то же время я видел достаточно много примеров, когда динамический полиморфизм использовался там, где за глаза хватало статического — просто по причине консервативности мышления.
--
Re[5]: Не очень понимаю, как на модных плюсиках сделать таку
Начиная с С++17, можно и без этой дополнительной фунции — просто класс AtExit делается шаблонным и дальше его можно будет использовать без явной спецификации шаблонного параметра, благодаря CTAD: