Политический подход с регистратором
Я трачу некоторое время на изменение дизайна класса логгера, который я однажды сделал в подходе, основанном на политике, после прочтения статьи о разработке на основе политики и желания попробовать что-то сам.
Некоторый код:
template <class Filter, class Formatter, class Outputter>
class LoggerImpl : public LoggerBase {
public:
LoggerImpl(const Filter& filter = Filter(), const Formatter& formatter = Formatter(), const Outputter& outputter = Outputter());
~LoggerImpl();
void log(int channel, int loglevel, const char* msg, va_list list) const;
private:
const Filter mFilter;
const Formatter mFormatter;
const Outputter mOutputter;
};
template <class Filter, class Formatter, class Outputter>
LoggerImpl<Filter, Formatter, Outputter>::LoggerImpl(const Filter& filter, const Formatter& formatter, const Outputter& outputter) :
mFilter(filter), mFormatter(formatter), mOutputter(outputter) {
debuglib::logdispatch::LoggerMgr.addLogger(this);
}
typedef LoggerImpl<NoFilter, SimpleFormatter, ConsoleOutputter> ConsoleLogger;
typedef LoggerImpl<ChannelFilter, SimpleFormatter, VSOutputter> SimpleChannelVSLogger;
typedef LoggerImpl<NoFilter, SimpleFormatter, FileOutputter> FileLogger;
ConsoleLogger c;
SimpleChannelVSLogger a(const ChannelFilter(1));
FileLogger f(NoFilter(), SimpleFormatter(), FileOutputter("log.txt"));
// macro for sending log message to all current created loggers
LOG(1, WARN, "Test %d", 1423);
В зависимости от регистратора мне нужно передать дополнительную информацию, такую как logchannel в SimpleChannelVsLogger или имя файла журнала в FileOututter.
Я передаю параметры конструктору LoggerImpl в качестве константной ссылки и впоследствии копирую их в объект, сохраненный в классе logger. Существует необходимость в их копировании, поскольку расширение времени жизни не является транзитивным через аргумент функции, возникающий при привязке временного созданного объекта к ссылке на const (подробнее об этом здесь: продлевает ли ссылка на const срок действия временной?).
Итак, первое, что нужно сделать: если я не хочу использовать указатели, так как меня не интересует распределение во время выполнения при использовании шаблонов, я думаю, что нет другого решения, кроме копирования временно созданных объектов, как описано выше?
Реальная проблема в копировании теперь связана с FileOutputter: конечно, нельзя скопировать ofstream, так как я могу скопировать объект FileOutputter, содержащий поток? Я пришел к следующему решению, чтобы преодолеть эту проблему:
struct FileOutputter {
// c_tor
FileOutputter() {}
// c_tor
explicit FileOutputter(const char* fname) {
mStream = std::make_shared<std::fstream>(fname, std::fstream::out);
}
// The copy c_tor will be invoked while creating any logger with FileOutputter
// FileLogger f(NoFilter(), SimpleFormatter(), FileOutputter("log.txt"));
// as all incoming paramters from the constructors stack frame are copied into the current instance of the logger
// as copying a file-stream is not permitted and not good under any means
// a shared pointer is used in the copy c_tor
// to keep the original stream until no reference is referring to it anymore
FileOutputter(const FileOutputter& other) {
mStream = other.mStream;
}
~FileOutputter() {
}
void out(const char* msg) const {
*mStream << msg;
}
std::shared_ptr<std::fstream> mStream;
};
Почему-то мне кажется, что это кажется немного сложным для "простого класса регистратора", однако в этом случае это может быть просто "проблемой" с подходом проектирования на основе политик.
Любые мысли приветствуются
2 ответа
Это правильно, что вы должны копировать объекты, если вы собираетесь хранить их в качестве членов в вашем классе.
Хранение ссылок опасно, так как в ваш ctor можно передавать временные объекты в качестве параметров, что приведет к зависанию ссылок при разрушении временных файлов.
Передача параметров в качестве указателей является альтернативой, но этот подход также проблематичен, так как тогда становится возможным передать в nullptr
(NULL-значение), и вы должны проверить это.
Другой альтернативой может быть перемещение значений, то есть передача параметров в виде ссылок на r-значения. Это позволит избежать копирования, однако для этого потребуется, чтобы клиент проходил временные или std::move
объекты при вызове ctor. Больше не будет возможности передавать ссылки на l-значение.
// Define ctor as taking r-value references.
template <class Filter, class Formatter, class Outputter>
LoggerImpl<Filter, Formatter, Outputter>::LoggerImpl(Filter&& filter, Formatter&& formatter, Outputter&& outputter) :
mFilter(std::move(filter)), mFormatter(std::move(formatter)), mOutputter(std::move(outputter)) {
// ...
}
/* ... */
// Calling ctor.
FileLogger f1(NoFilter(), SimpleFormatter(), FileOutputter("log.txt")); // OK, temporaries.
FileOutputter fout("log.txt");
FileLogger f2(NoFilter(), SimpleFormatter(), fout); // Illegal, fout is l-value.
FileLogger f3(NoFilter(), SimpleFormatter(), std::move(fout)); // OK, passing r-value. fout may not be used after this!
Если вы решили использовать подход копирования, тогда я рекомендую вместо этого передавать ваши параметры по значению в ctor. Это позволит компилятору выполнять оптимизацию как копирование (читайте: Хотите скорость? Проход по значению).
template <class Filter, class Formatter, class Outputter>
LoggerImpl<Filter, Formatter, Outputter>::LoggerImpl(Filter filter, Formatter formatter, Outputter outputter) :
mFilter(std::move(filter)), mFormatter(std::move(formatter)), mOutputter(std::move(outputter)) {
// ...
}
Используя приведенное выше определение: в лучшем случае компилятор удалит копию, а члены будут построены (при передаче временного объекта).
В худшем случае: будет выполнено копирование и построение перемещения (при передаче l-значения).
Используя вашу версию (передавая параметры как ссылку на const), копия всегда будет выполняться, так как компилятор не может выполнить оптимизацию.
Чтобы конструкция перемещения работала, вам нужно убедиться, что типы, передаваемые в качестве параметров, являются конструктивными для перемещения (либо неявно, либо с использованием объявленного ctor-перемещения). Если тип не является перемещаемым, он будет создан для копирования.
Когда дело доходит до копирования потока в FileOutputter
, с помощью std::shared_ptr
кажется хорошим решением, хотя вы должны инициализировать mStream
в списке инициализации вместо назначения в теле ctor:
explicit FileOutputter(const char* fname)
: mStream(std::make_shared<std::ofstream>(fname)) {}
// Note: Use std::ofstream for writing (it has the out-flag enabled by default).
// There is another flag that may be of interest: std::ios::app that forces
// all output to be appended at the end of the file. Without this, the file
// will be cleared of all contents when it is opened.
std::ofstream
не подлежит копированию и передает интеллектуальный указатель (обязательно используйте std::shared_ptr
хотя), вероятно, самое простое решение в вашем случае, а также, на мой взгляд, вопреки тому, что вы говорите, не слишком сложное.
Другим подходом было бы сделать статический член потока, но затем каждый экземпляр FileOutputter
будет использовать то же самое std::ofstream
объект, и было бы невозможно использовать параллельные объекты журнала, записывающие в разные файлы и т. д.
В качестве альтернативы вы можете переместить поток как std::ofstream
не копируемый, но подвижный. Это, однако, потребует от вас сделать FileOutputter
подвижные и не копируемые (и, вероятно, LoggerImpl
также), поскольку использование "перемещенного" объекта, кроме его dtor, может привести к UB. Хотя создание объекта, который управляет типами "только для перемещения", само по себе становится "только для перемещения", иногда имеет большой смысл.
std::ofstream out{"log.txt"};
std::ofstream out2{std::move(out)} // OK, std::ofstream is moveable.
out2 << "Writing to stream"; // Ok.
out << "Writing to stream"; // Illegal, out has had its guts ripped out.
Кроме того, в приведенном примере вам не нужно объявлять копию ctor или dtor для FileOutputter
, поскольку они будут неявно сгенерированы компилятором.
Вы можете иметь классы политики, содержащие статические функции, поэтому в идеале вы бы хотели, чтобы FileOutputter выглядел так:
template<std::string fileName>
struct FileOutputter {
static void out(const char* msg) const {
std::ofstream out(fileName);
out << msg;
}
};
Вы должны создать экземпляр LoggerImpl, как это
LoggerImpl<NoFilter, SimpleFormatter, FileOutputter<"log.txt"> > FileLogger;
Это будет означать, что вашему LoggerImpl не нужно хранить копию классов политик, которые ему просто необходимы для вызова их статических функций. К сожалению, это не сработает, потому что вы не можете иметь строки в качестве аргументов шаблона, но вы можете построить таблицу строк и передать индекс имени файла в вашей таблице строк. Опять же, в идеале вы бы хотели, чтобы это выглядело так:
//This class should be a singleton
class StringManager
{
std::vector<std::string> m_string;
public:
void addString(const std::string &str);
const std::string &getString(int index);
int getIndexOf(const std::string &str);
};
Тогда ваш FileLogger получит int в качестве параметра шаблона, и это будет индекс строки в StringManager. Это также не совсем работает, потому что вам нужен индекс, доступный во время компиляции, и StringManager будет инициализирован во время выполнения. Таким образом, вам нужно будет создать таблицу строк вручную и вручную записать в индекс вашей строки. поэтому ваш код будет выглядеть (после того, как вы сделаете StringManager одиночным:
StringManager::instance()->addString("a");
StringManager::instance()->addString("b");
StringManager::instance()->addString("c");
StringManager::instance()->addString("d");
StringManager::instance()->addString("log.txt");
LoggerImpl<NoFilter, SimpleFormatter, FileOutputter<4> > FileLogger;
Вы должны убедиться, что StringManager полностью инициализирован перед созданием первого экземпляра FileLogger. Это немного уродливо, но использование шаблонов со строками немного уродливо. Вы также можете сделать что-то вроде:
template <class FilenameHolder>
struct FileOutputter {
static void out(const char* msg) const {
std::ofstream out(FilenameHolder::fileName());
out << msg;
}
};
class MyFileNameHolder
{
static std::string fileName() {return "log.txt";}
};