基于策略的方法与记录器

时间:2013-11-29 00:29:17

标签: c++ logging ofstream policy-based-design

我花了一些时间在阅读一篇关于基于策略的设计并希望自己尝试一些东西的文章之后,重新设计我曾做过一次基于策略的方法的记录器类。

一些代码:

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);

根据记录器的不同,我需要传递其他信息,例如SimpleChannelVsLogger中的logchannel或FileOututter中日志文件的文件名。

我将参数传递给LoggerImpl的构造函数作为const引用,然后将它们复制到logger类中存储的对象中。 需要复制它们,因为生命周期扩展不能通过将临时创建的对象绑定到const引用时发生的函数参数传递(此处更多内容:Does a const reference prolong the life of a temporary?)。

首先在这里:如果我不想使用指针,因为我在使用模板时对运行时分配不感兴趣,我想除了像上面那样复制临时创建的对象之外没有其他解决方案吗?

复制内容中的实际问题现在随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 个答案:

答案 0 :(得分:4)

如果要将对象存储为班级中的成员,那么复制对象是正确的。

存储引用是危险的,因为可以将临时对象作为参数传递给ctor,这会在临时对象被破坏时导致悬空引用

将参数作为指针传递是另一种选择,但是这种方法也存在问题,因为它可以传入nullptr(NULL值),你必须检查它。

另一种选择是移动值,即将参数作为r值引用传递。这将避免复制,但是它需要客户端在调用ctor时传递临时对象或std::move对象。不可能传递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中按值传递参数。这将允许编译器以 copy elision 执行优化(读取:Want Speed? Pass by Value)。

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.

此外,在提供的示例中,您不需要为FileOutputter声明副本ctor或dtor,因为它们将由编译器隐式生成。

答案 1 :(得分:3)

您可以让策略类包含静态函数,因此理想情况下您希望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;

您必须确保在创建FileLogger的第一个实例之前完全初始化StringManager。 这有点难看,但使用带字符串的模板有点难看。 你也可以这样做:

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";}
};