Типобезопасный метод для получения данных неизвестного типа через интерфейс

Версия TL;DR:

Я разрабатываю класс на С++ 14, чтобы он был универсальным. Ниже я описываю проблему дизайна, и я был бы признателен за решение для реализации того, что я пытаюсь, или предложение по редизайну.

Скажем, класс, который я разрабатываю, называется Algo. Его конструктору передается unique_ptr типу, скажем, Business, который реализует интерфейс (то есть наследуется от чистого виртуального класса) и выполняет большую часть серьезной работы.

Я хочу, чтобы объект типа Algo мог возвращать указатель (или даже копию) члена данных из объекта Business, которым он владеет. Но он не может знать тип, который Business захочет вернуть. Я ожидаю, что владелец Algo будет знать, что получится, основываясь на том, что Business он передал.

Когда я работал на C, я бы избавился от системы типов, передавая void* и выполняя кастинг по мере необходимости. Но такие вещи меня сейчас бесят.

Подробнее:

Итак, своего рода псевдо-C++14 реализация описанной выше ситуации может выглядеть так:

// perhaps a template here?
class AbstractBusiness {
  . . .
 public:
  ?unknownType? result();
};

class Algo {
  //Could be public if needbe.
  unique_ptr<AbstractBusiness> concreteBusiness_;

 public:
  Algo(std::unique_ptr<AbstractBusiness> concreteBusiness);
  auto result() {return concreteBusiness_.result();}
};

class Business : public AbstractBusiness {
  . . .
 public:
  std::valarray<float> data_;
  std::valarray<float> result() {return data_;}
};

:::

auto b = std::unique_ptr<AbstractBusiness>{std::move(new Business())};
Algo a(std::move(b));
auto myResult = a.result();

В этом примере myResult будет std::valarray<float>, но я не хочу, чтобы Algo или интерфейс AbstractBusiness знали об этом! Создатель b и a должен знать, что должно получиться из a.result().

Если я ошибаюсь в этом дизайне, не стесняйтесь, дайте мне знать. Я немного зеленый на данный момент и очень открыт для предложений.

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

Я играю с идеей создания интерфейса контейнера для всего, что возвращает Business.result(), и просто передачи указателей на абстрактный тип до Algo.result(). Но я начинаю чувствовать, что может быть лучший способ, поэтому я здесь прошу предложений.


person Timtro    schedule 09.10.2015    source источник
comment
Могут ли все возможные типы результатов иметь общий класс-предок?   -  person Kuba Wyrostek    schedule 09.10.2015
comment
@KubaWyrostek Да, я упоминаю внизу вопроса, что рассматриваю возможность создания абстрактного типа контейнера и передачи указателей на абстрактный тип от concreteBusiness до Algo.return(). Это то, что вы предлагаете, или я неправильно понял?   -  person Timtro    schedule 10.10.2015
comment
Вообще-то нет. Даже если вы это сделаете - то, что Algo может вернуть, это только некоторое AbstractData, которое требует другого приведения. Это не сильно отличается от void*. Учитывая, что Algo, очевидно, не знает, что данные, возвращаемые Business, означают, поэтому он их не использует. Нужно ли ему принимать участие в возврате результата? Вызывающий точно знает тип данных, используемых в конкретном потомке AbstractBusiness, так почему бы просто не спросить этого потомка?   -  person Kuba Wyrostek    schedule 10.10.2015
comment
@KubaWyrostek Возможно, вы правы — возможно, я виноват в том, что слишком старался защитить concreteBusiness. Я пытался сохранить это в тайне, но, возможно, мне это не нужно, и я просто слишком стараюсь ради метода «добытчика». Я также должен сказать, что a.concreteBusiness->data_ немного уродлив, но не нарушает договоренности.   -  person Timtro    schedule 10.10.2015
comment
a.concreteBusiness->data_ не будет работать, так как вы все еще не знаете тип concreteBusiness здесь. Только самый внешний вызывающий объект знает о типе.   -  person Kuba Wyrostek    schedule 10.10.2015
comment
@KubaWyrostek :0 Ты прав. Я немного забежал вперед. Так что именно вы предлагаете?   -  person Timtro    schedule 10.10.2015
comment
В этой строке: auto b = std::unique_ptr<AbstractBusiness>{std::move(new Business())}; вы теряете информацию о фактическом типе вовлеченного AbstractBusiness. Но если вы сохраните ссылку на new Business(), то компилятор прекрасно понимает, что _data в этой ссылке имеет тип std::valarray<float>.   -  person Kuba Wyrostek    schedule 10.10.2015
comment
Рассматривали ли вы Boost.Any? Подобно std::function, он позволяет вам извлекать базовые данные только в том случае, если вы знаете тип.   -  person melak47    schedule 10.10.2015


Ответы (2)


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

Вы сообщаете нам, что Algo получает право собственности на бизнес через указатель на полиморфный интерфейс AbstractBusiness и должен предоставить геттер для данных этого бизнеса, хотя ему не известен конкретный тип этих данных (поскольку он не знает конкретный тип бизнеса).

Ни на один из этих вопросов нет очевидных ответов:

  1. Почему Algo должен приобретать бизнес через полиморфный интерфейс?
  2. Почему Algo должен предоставлять геттер для данных своего бизнеса?

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

Полиморфная выбоина и как из нее выбраться

Q1. заставляет нас задаться вопросом, какова мотивация AbstractBusiness? Банально, можно с уверенностью сказать, что вы хотите, чтобы он предоставлял единый интерфейс для манипулирования и запросов всех видов бизнеса конкретных типов, которые могут быть определены во время выполнения.

Чтобы полностью соответствовать этой цели, AbstractBusiness будет инкапсулировать необходимый и достаточный интерфейс для выполнения всех операций и запросов по конкретным предприятиям, которые, как разумно ожидать, могут понадобиться приложениям (включая, помимо прочего, ваши собственные). Назовите этот план А. Вы обнаружили, что он не полностью подходит для плана А. Если приложению необходимо время от времени манипулировать или запрашивать «данные» бизнеса, которые представлены ему через AbstractBusiness, тогда интерфейс AbstractBusiness должен предоставлять полиморфные методы для выполнения всех этих манипуляций и запросов, и каждый конкретный бизнес-класс должен реализовать их соответствующим образом для типа содержащихся в нем данных.

Где у вашего AbstractBusiness проблема:

?unknownType? result();

вам нужно закодировать виртуальные методы, отвечающие на все убедительные ответы на вопрос: Что приложение может захотеть узнать о условном result() или сделать с ним?

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

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

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

AbstractBusiness будет представлять только тот интерфейс, который используется всеми предприятиями. У него вообще не будет result(), и вызывающий объект, получивший указатель на AbstractBusiness с намерением сделать что-то с вещью, возвращенной BusinessTypeX::result(), продолжит динамическое приведение исходного указателя к BusinessTypeX * и вызов result() через целевой указатель только в том случае, если его ненулевой.

Мы до сих пор не знаем, какова мотивация AbstractBusiness. Мы только что выдвинули довольно правдоподобную мысль о том, что у вас есть «хрестоматийные» амбиции по этому поводу — план А — и либо не поняли, что вы просто не закончили его, либо поняли, что разнообразие данных, которые вы работа с не позволяет вам завершить его в соответствии с планом А и не иметь плана Б. План Б заключается в следующем: углубить полиморфную иерархию и использовать dynamic_cast<LowerType *>(HigherType *) для обеспечения безопасного доступа к LowerType интерфейсу, когда он опережает HigherType. [1]

На очереди Q2. в настоящее время. Скорее всего, причина для Algo::result() проста: потому что класс готов предоставить геттеры, которые напрямую отвечают на естественные запросы клиента, и в этом случае естественный запрос предназначен для данных, принадлежащих бизнесу, которому принадлежит по Algo. Но если Algo знает свой бизнес только как AbstractBusiness, то он просто не может вернуть данные, принадлежащие его бизнесу, потому что уже увиденные причины означают, что AbstractBusiness не может вернуть "данные" в Algo или что-то еще.

Algo::result() неправильно понимается так же, как AbstractBusiness::result() неправильно понимается. Учитывая, что данные BusinessXs и BusinessYs, возможно, потребуется запрашивать либо с помощью некоторого репертуара виртуальных методов, которые все еще TODO в AbstractBusiness (план A), либо, возможно, с помощью методов BusinessX и BusinessY, которые вообще не унаследованы от AbstractBusiness (план B). , единственный запрос, который Algo, безусловно, может и должен поддерживать в отношении своего бизнеса, состоит в том, чтобы вернуть указатель AbstractBusiness, через который он владеет своим бизнесом, предоставляя вызывающей стороне возможность запросить через указатель или понизить его, если они могут, до более низкого уровня. интерфейс, который они хотят запросить. Даже если можно закончить AbstractBusiness по плану А, идея о том, что отсутствующий отчет о методах должен быть продублирован в интерфейсе Algo только для того, чтобы вызывающему объекту никогда не приходилось получать и понижать указатель AbstractBusiness, неубедительна. Будет ли этому соответствовать каждый тип, который управляет указателем AbstractBusiness?

Подводя итог, если у AbstractBusiness есть веская причина для существования, то вам нужно либо закончить его в соответствии с Планом А и проработать последствия этого, либо сократить его до попытки стать достаточным интерфейсом для управления всеми предприятиями и подкрепите его расширенной полиморфной иерархией, которую клиенты согласовывают с помощью динамического приведения согласно плану Б; и в любом случае вы должны довольствоваться Algo и подобными заданиями в торговле AbstractBusiness только для того, чтобы вернуть их указатель AbstractBusiness клиентам, которые специально его используют.

Лучше не ходите туда

Но вопрос о том, есть ли у AbstractBusiness веская причина для существования, все еще нерешен, и если вы обнаружите, что вынуждены прибегнуть к плану Б, это само по себе сделает вопрос более острым: когда выяснится, что абстрактный интерфейс, брошенный как корневой класс единой иерархии наследования, не может предоставить план А, тогда возникает сомнение в мудрости архитектуры, которую он изображает. Динамическое приведение для обнаружения и получения интерфейсов является неуклюжим и дорогим способом управления потоком и особенно досадным, когда - как вы нам говорите, это ваша ситуация - область, которая должна будет выполнять канцелярскую канитель, уже знает тип, который она должна "выйти" тип, который он "вставил". Должны ли все типы, которые являются несовершенными потомками корневой абстракции, иметь одного предка по причине, иной, чем единообразие интерфейса (поскольку это не дает им этого)? Экономия универсальных интерфейсов — постоянная цель, но является ли полиморфизм времени выполнения правильным средством или хотя бы одним из правильных средств для их реализации в контексте вашего проекта?

В вашем наброске кода AbstractBusiness служит не какой-либо конечной цели, а предоставляет тип, который может единообразно заполнять определенные слоты в классе Algo, в результате чего Algo может правильно работать с любым типом, который демонстрирует определенные черты и поведение. Как показано, единственное требование Algos к уточняющему типу состоит в том, что он должен иметь метод result(), который что-то возвращает: ему все равно что. Но тот факт, что вы выражаете требования Algos к уточняющему типу, указывая, что он должен быть AbstractBusiness, запрещает не заботиться о том, что возвращает result(): AbstractBusiness не может выполнять этот result() метод, хотя любой из его потомков может сделать.

Предположим в этом случае, что вы увольняете AbstractBusiness с работы по обеспечению общих атрибутов типов, с которыми может работать Algo, и вместо этого позволяете Algo делать это самому, сделав его шаблоном? - поскольку похоже, что то, что AbstractBusiness делает для Algo, служит цели параметра шаблона, но саботирует эту самую цель:

#include <memory>

template<class T>
class Algo {
    std::unique_ptr<T> concreteBusiness_;

public:
    explicit Algo(T * concreteBusiness)
    : concreteBusiness_{concreteBusiness}{};
    auto result() { return concreteBusiness_->result(); }
};

#include <valarray>
#include <algorithm>

struct MathBusiness {
    std::valarray<float> data_{1.1,2.2,3.3};
    float result() const { 
        return std::accumulate(std::begin(data_),std::end(data_),0.0);
    }
};

#include <string>

struct StringBusiness {
    std::string data_{"Hello World"};
    std::string result() const { return data_; }
};

#include <iostream>

int main()
{
    Algo<MathBusiness> am{new MathBusiness};
    auto ram = am.result();
    Algo<StringBusiness> as{new StringBusiness};
    auto ras = as.result();
    std::cout << ram << '\n' << ras << '\n';
    return 0;
}

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

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

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


[1] Другой план может выглядеть так: инкапсулировать «данные» каждой конкретной компании в boost::any или std::experimental::any. Но вы, вероятно, сразу видите, что это по сути то же самое, что и идея инкапсулировать данные в абстракцию спасения, используя готовую абстракцию швейцарской армии, а не создавать свою собственную. В любом виде эта идея по-прежнему оставляет звонящим возможность понизить абстракцию до типа реального интереса, чтобы выяснить, есть ли у них это, и в этом смысле это вариант плана Б.

person Mike Kinghan    schedule 11.10.2015
comment
Извините за задержку с ответом. Вы дали мне много пищи для размышлений, так что я займусь этим. Что касается вашего комментария о том, что это не проблема дизайна, ваша точка зрения понятна. - person Timtro; 13.10.2015
comment
Что ж, извините за необычную длину. Надеюсь, это хотя бы поможет вам справиться с вашей проблемой. - person Mike Kinghan; 13.10.2015
comment
Не извиняйся, пожалуйста. Вы действительно помогли мне обдумать дизайнерские решения, которые я принял, и из-за этого я меняю свой дизайн. Но ваши решения тоже не напрасны, я нашел их очень информативными. std::future был особенно точен. Вы можете сказать, что я новичок в ОО. Я научный программист из области, которая считала меня бунтарем из-за того, что я программист на C, а не на Fortran. Я был бы анархистом, даже если бы использовал C с классами! Я увидел свет, и моя копия GOF прибыла только на прошлой неделе :) - person Timtro; 14.10.2015

Есть несколько способов сделать это. Самый простой способ — не передавать право собственности, а вызывать Algo по ссылке:

Business b;
Algo(b);
auto result = b.get_result();

Однако иногда это невозможно. В этом случае открываются различные варианты, которые могут стать довольно сложными. Начну с самого универсального и сложного:

Если вы знаете все типы, производные от AbstractBusiness, вы можете использовать шаблон посетителя:

Сначала мы объявляем абстрактный метод accept в AbstractBusiness, который принимает BusinessVisitor. Этот visitor будет отвечать за обработку различных типов и выполнение действия в зависимости от того, какой тип он посещает:

class BusinessVisitor;

struct AbstractBusiness {
  virtual ~AbstractBusiness() = default;
  virtual void accept(BusinessVisitor&) const = 0;
};

BusinessVisitor выглядит так:

class BusinessOne;
class BusinessTwo;

struct BusinessVisitor {
  virtual ~BusinessVisitor() = default;
  virtual void on_business_one(const BusinessOne&) {};
  virtual void on_business_two(const BusinessTwo&) {};
};

Некоторые люди предпочитают вызывать все методы посетителя visit, а разрешение перегрузки сделает все остальное, но я предпочитаю более явные имена.

struct BusinessOne {
  void accept(BusinessVisitor& v) const {
    v.on_business_one(*this);
  }
};

struct BusinessTwo {
  void accept(BusinessVisitor& v) const override {
    v.on_business_two(*this);
  }
};

Теперь мы можем добавить метод accept к Algo. Этот просто отправит содержащийся объект AbstractBusiness.

class Algo {
  std::unique_ptr<AbstractBusiness> b_;
 public:
  Algo(std::unique_ptr<AbstractBusiness> b);
  void accept(BusinessVisitor& visitor) const override {
    return b_->accept(visitor);
  }
};

Чтобы получить результат для определенного типа бизнеса, нам нужно определить посетителя, который обрабатывает этот тип:

struct BusinessOneResult : public BusinessVisitor {
  void on_business_one(const BusinessOne& b) {
    // save result;
  }

  /* ... */ get_result() const;
};

Теперь мы можем запустить Algo и получить результат:

auto b = std::unique_ptr<AbstractBusiness>(new BusinessOne());
Algo a(std::move(b));
BusinessOneResult visitor; 
a.accept(visitor);
auto result = visitor.get_result();

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

Другим и довольно элегантным способом было бы использование std::future:

struct Business {
  std::future</*...*/> get_future_result() {
    return promise_.get_future();
  }

  void some_method() {
    // ...
    promise_.set_value(...);
  }

 private:
  std::promise</*...*/> promise_;
};

// Must use Business here (AbstractBusiness doesn't know about the
// type of the future).
auto b = std::unique_ptr<Business>(new Business());
auto future = b.get_future_result();
Algo a(std::move(b));
auto result = future.get();

Другим способом было бы обернуть тип в класс, производный от класса тега (без методов или членов данных) и dynamic_cast к типу, который, как вы знаете, он содержит. Использование dynamic_cast обычно не одобряется, но у него есть свое применение.

std::any или boost::any был бы другим способом.

Примечание. Я удалил std::move для аргумента конструктора std::unique_ptr, он там ничего не делает: результатом операции new уже является значение r, и перемещение указателя так же эффективно, как и его копирование. .

person Florian    schedule 09.10.2015
comment
Business b(); объявить функцию Business b(). - person Tomilov Anatoliy; 11.10.2015
comment
Приносим извинения за поздний ответ. Спасибо --- я проработаю все это и посмотрю, что подходит. Прямо сейчас я склоняюсь к вашему первому предложению даже не передавать права собственности. - person Timtro; 13.10.2015
comment
@Florian Флориан, я действительно хочу поблагодарить тебя за твой ответ. Я не могу преувеличить, насколько полезным я нашел это. В конце концов, я изменил свой дизайн, потому что понял, что пытаюсь использовать полиморфизм там, где это неуместно. Я новичок в C ++ (я много лет был программистом на C), я нашел ваш ответ действительно очень поучительным. Настолько, что я его распечатал. - person Timtro; 14.10.2015
comment
@Timtro Рад, что смог помочь. :D - person Florian; 14.10.2015