Я провожу некоторое время с редизайном класса логгера, который я однажды сделал в подходе, основанном на политике, после прочтения статьи о разработке на основе политики и желания попробовать что-то сам.
Некоторый код:
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 в качестве константной ссылки и впоследствии копирую их в объект, сохраненный в классе регистратора.
Существует необходимость в их копировании, поскольку расширение времени жизни не является транзитивным через аргумент функции, возникающий при привязке временно созданного объекта к константной ссылке (подробнее об этом здесь: Продлевает ли константная ссылка временную жизнь?).
Итак, первое, что нужно сделать: если я не хочу использовать указатели, поскольку меня не интересует распределение во время выполнения при использовании шаблонов, я думаю, что нет другого решения, кроме копирования временно созданных объектов способом, описанным выше?
Фактическая проблема в копировании теперь происходит с FileOutputter:
Конечно, офстрим не может быть скопирован, так как я могу скопировать объект 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;
};
Почему-то мне кажется, что это кажется немного сложным для «простого класса логгера», однако в этом случае это может быть просто «проблемой» с подходом проектирования на основе политик.
Любые мысли приветствуются
Это правильно, что вы должны копия объекты, если вы собираетесь хранить их в качестве членов в вашем классе.
Хранение ссылок опасно, так как можно передавать временные объекты в качестве параметров вашему 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. Это позволит компилятору выполнять оптимизацию как копия elision (читать: Хотите скорость? Передать по значению).
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";}
};