基于策略的方法与记录器

Chr*_*ris 6 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);
Run Code Online (Sandbox Code Playgroud)

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

我将参数作为const引用传递给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;
        };
Run Code Online (Sandbox Code Playgroud)

不知怎的,我不得不觉得这对于"简单的记录器类"来说似乎有点复杂,但在这种情况下,这可能仅仅是基于策略的设计方法的"问题".

欢迎任何想法

Snp*_*nps 4

如果要将对象作为成员存储在类中,则应该复制这些对象,这是正确的。

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

将参数作为指针传递是一种替代方法,但这种方法也有问题,因为这样就可以传入nullptr(NULL 值),并且您必须检查这一点。

另一种选择是移动值,即将参数作为右值引用传递。std::move这将避免复制,但是它将要求客户端在调用构造函数时传递临时对象或对象。将不再可能传递左值引用。

// 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!
Run Code Online (Sandbox Code Playgroud)

如果您决定采用复制方法,那么我建议您在构造函数中按值传递参数。这将允许编译器执行复制省略的优化(阅读:想要速度?按值传递)。

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)) {
            // ...
}
Run Code Online (Sandbox Code Playgroud)

使用上面的定义:在最好的情况下,编译器将删除副本,并且将移动构造成员(当传递临时对象时)。

在最坏的情况下:将执行复制和移动构造(当传递左值时)。

使用您的版本(传递参数作为 const 的引用),将始终执行复制,因为编译器无法执行优化。

为了使移动构造起作用,您必须确保作为参数传递的类型是可移动构造的(隐式或使用声明的移动构造函数)。如果类型不可移动构造,它将被复制构造。

当涉及到复制流时FileOutputter,使用std::shared_ptr似乎是一个很好的解决方案,尽管您应该mStream初始化列表中初始化而不是在构造函数主体中分配:

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.
Run Code Online (Sandbox Code Playgroud)

Astd::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.
Run Code Online (Sandbox Code Playgroud)

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