Полиморфная переменная-член класса
У меня есть класс messenger
, который опирается на экземпляр printer
. printer
- это полиморфный базовый класс, и фактический объект передается messenger
в конструкторе.
Для неполиморфного объекта я бы просто сделал следующее:
class messenger {
public:
messenger(printer const& pp) : pp(pp) { }
void signal(std::string const& msg) {
pp.write(msg);
}
private:
printer pp;
};
Но когда printer
является полиморфным базовым классом, это больше не работает (нарезка).
Каков наилучший способ сделать эту работу, учитывая, что
- Я не хочу передавать указатель на конструктор и
- Класс
printer
не должен иметь виртуальный метод clone
(= должен полагаться на построение копии).
Я не хочу передавать указатель на конструктор, потому что остальная часть API работает с реальными объектами, а не с указателями, и было бы непонятно, что здесь есть указатель в качестве аргумента.
В С++ 0x я мог бы использовать unique_ptr
вместе с конструктором шаблонов:
struct printer {
virtual void write(std::string const&) const = 0;
virtual ~printer() { } // Not actually necessary …
};
struct console_printer : public printer {
void write(std::string const& msg) const {
std::cout << msg << std::endl;
}
};
class messenger {
public:
template <typename TPrinter>
messenger(TPrinter const& pp) : pp(new TPrinter(pp)) { }
void signal(std::string const& msg) {
pp->write(msg);
}
private:
std::unique_ptr<printer> pp;
};
int main() {
messenger m((console_printer())); // Extra parens to prevent MVP.
m.signal("Hello");
}
Это лучшая альтернатива? Если да, то какой был бы лучший способ в pre-0x? И есть ли способ избавиться от совершенно ненужной копии в конструкторе? К сожалению, перемещение временного не работает здесь (правильно?).
Ответы
Ответ 1
Невозможно клонировать полиморфный объект без метода виртуального клонирования. Таким образом, вы можете:
- передайте и удерживайте ссылку и убедитесь, что принтер не был уничтожен перед мессенджером в кодовом конструировании мессенджера,
- передать и удерживать интеллектуальный указатель и создать экземпляр принтера с новым,
- передать ссылку и создать экземпляр экземпляра в куче, используя метод clone или
- передать ссылку на фактический тип в шаблон и создать экземпляр с новым, пока вы все еще знаете тип.
Последнее, что вы предлагаете с С++ 0x std::unique_ptr
, но в этом случае С++ 03 std::auto_ptr
сделает вам точно такую же службу (т.е. вам не нужно ее перемещать, иначе они то же).
Изменить: Ok, um, еще один способ:
- Сделать
printer
сам умным указателем на фактическую реализацию. Затем он копируется и полиморфно в то же время за счет некоторой сложности.
Ответ 2
Unfortunately, moving the temporary doesn’t work here (right?).
Неправильно. Быть, глупо. Это то, на что ссылаются ссылки rvalue. Простая перегрузка быстро решит проблему.
class messenger {
public:
template <typename TPrinter>
messenger(TPrinter const& pp) : pp(new TPrinter(pp)) { }
template <typename TPrinter>
messenger(TPrinter&& pp) : pp(new TPrinter(std::move(pp))) { }
void signal(std::string const& msg) {
pp->write(msg);
}
private:
std::unique_ptr<printer> pp;
};
Та же концепция будет применяться в С++ 03, но замените unique_ptr
на auto_ptr
и отбросьте опорную перегрузку rvalue.
Кроме того, вы можете рассмотреть какой-то конструктор "dummy" для С++ 03, если вы в порядке с небольшим изворотливым интерфейсом.
class messenger {
public:
template <typename TPrinter>
messenger(TPrinter const& pp) : pp(new TPrinter(pp)) { }
template<typename TPrinter> messenger(const TPrinter& ref, int dummy)
: pp(new TPrinter())
{
}
void signal(std::string const& msg) {
pp->write(msg);
}
private:
std::unique_ptr<printer> pp;
};
Или вы могли бы рассмотреть ту же стратегию, что auto_ptr
использует для "перемещения" в С++ 03. Для использования с осторожностью, конечно, но совершенно законным и выполнимым. Проблема в том, что вы влияете на все подклассы printer
.
Ответ 3
Расширение комментария в правильный ответ...
Первоочередной задачей здесь является право собственности. Из вашего кода кажется, что каждый экземпляр messenger
имеет собственный экземпляр принтера, но на самом деле вы передаете готовый принтер (предположительно с некоторым дополнительным состоянием), который вам нужно затем скопировать в свой собственный экземпляр of printer
. Учитывая предполагаемый характер объекта printer
(т.е. Для печати чего-либо), я бы сказал, что вещью, к которой он относится, является общий ресурс - в этом свете нет смысла для каждого экземпляра messenger
иметь его собственная копия printer
(например, что, если вам нужно заблокировать доступ к std::cout
)?
С точки зрения дизайна, то, что messenger
требуется для построения, на самом деле действительно является указателем на какой-то общий ресурс - в этом свете лучше shared_ptr
(лучше еще weak_ptr
).
Теперь, если вы не хотите использовать weak_ptr
, и вы предпочитаете хранить ссылку, подумайте о том, можете ли вы messenger
подключиться к типу printer
, связь остается у пользователя, вам все равно - конечно, основным недостатком этого является то, что messenger
не будет сдерживаемым. ПРИМЕЧАНИЕ. Вы можете указать класс признаков (или политики), на который может быть введен тип messenger
, и это предоставляет информацию о типе для принтера (и может управляться пользователем).
Третья альтернатива заключается в том, что у вас есть полный контроль над набором принтеров, и в этом случае удерживайте вариант типа - он намного чище IMHO и избегает полиморфизма.
Наконец, если вы не можете подключиться, вы не можете управлять принтерами и хотите, чтобы ваш собственный экземпляр printer
(того же типа), шаблон конструктора преобразования - это путь вперед, однако добавьте disable_if
, чтобы предотвратить это называется неправильным (т.е. как нормальная копия ctor).
В целом я рассматривал бы принтер как общий ресурс и держал бы weak_ptr
, так как, честно говоря, он позволяет лучше контролировать этот общий ресурс.
Ответ 4
Почему вы не хотите передавать указатель или интеллектуальный указатель?
В любом случае, если вы всегда инициализируете член принтера в конструкторе, вы можете просто использовать ссылочный элемент.
private:
printer& pp;
};
И инициализируется в списке инициализации конструктора.
Ответ 5
Когда у вас есть золотой молот, все выглядит как гвозди.
Ну, мой последний золотой молот - стирание стилей. Серьезно, я бы не использовал его, но опять же, я бы передал указатель и создал вызывающий и добавлял зависимость.
struct printer_iface {
virtual void print( text const & ) = 0;
};
class printer_erasure {
std::shared_ptr<printer_iface> printer;
public:
template <typename PrinterT>
printer_erasure( PrinterT p ) : printer( new PrinterT(p) ) {}
void print( text const & t ) {
printer->print( t );
}
};
class messenger {
printer_erasure printer;
public:
messenger( printer_erasure p ) : printer(p) {}
...
};
Хорошо, возможно, это и решения, предоставляемые с шаблоном, - это то же самое, с единственной небольшой разницей, что сложность стирания типа перемещается за пределы класса. Класс messenger
имеет свои собственные обязанности, и стирание типа не является одним из них, оно может быть делегировано.
Ответ 6
Как насчет шаблонов class messanger
?
template <typename TPrinter>
class messenger {
public:
messenger(TPrinter const& obj) : pp(obj) { }
static void signal(printer &pp, std::string const& msg) //<-- static
{
pp->write(msg);
}
private:
TPrinter pp; // data type should be template
};
Обратите внимание, что signal()
сделано static
. Это должно использовать способность virtual
class printer
и не создавать новую копию signal()
. Единственное, что вам нужно сделать, - вызвать функцию, например,
signal(this->pp, "abc");
Предположим, что у вас есть другие типы данных, а затем pp
, которые не связаны с типом шаблона, тогда их можно перенести в базовый класс без шаблона, и эта база может быть унаследована messenger
. Я не описываю много деталей, но, я хочу, чтобы точка была более ясной.