mercredi 21 mars 2018

How to declare static information in scopes accessible to nested lexical scopes in C++?

I want to declare identifiers for scopes which will be used to automatically populate a field of any logging statements within the innermost scope. They will usually, but not always (e.g. lambdas, blocks introduced with {}), match the "name" of the enclosing block.

Usage would look something like this:

namespace app {

LOG_CONTEXT( "app" );

class Connector {
    LOG_CONTEXT( "Connector" );
    void send( const std::string &  msg )
    {
        LOG_CONTEXT( "send()" );
        LOG_TRACE( msg );
    }
};

} // namespace app

//                     not inherited
LOG_CONTEXT( "global", false );

void fn()
{
    LOG_DEBUG( "in fn" );
}

int main()
{
    LOG_CONTEXT( "main()" );
    LOG_INFO( "starting app" );
    fn();
    Connector c;
    c.send( "hello world" );
}

with the result being something like:

[2018-03-21 10:17:16.146] [info] [main()] starting app
[2018-03-21 10:17:16.146] [debug] [global] in fn
[2018-03-21 10:17:16.146] [trace] [app.Connector.send()] hello world

We can get the inner-most scope by defining the LOG_CONTEXT macro such that it declares a struct. Then in the LOG_* macros we call a static method on it to retrieve the name. We pass the whole thing to a callable object, e.g.:

namespace logging {

spdlog::logger & instance()
{
    auto sink =
        std::make_shared<spdlog::sinks::ansicolor_stdout_sink_mt>();
    decltype(sink) sinks[] = {sink};
    static spdlog::logger logger(
        "console", std::begin( sinks ), std::end( sinks ) );
    return logger;
}

// TODO: stack-able context
class log_context
{
public:
    log_context( const char *  name )
        : name_( name )
    {}

    const char * name() const
    { return name_; }

private:
    const char *  name_;
};

class log_statement
{
public:
    log_statement( spdlog::logger &           logger,
                   spdlog::level::level_enum  level,
                   const log_context &        context )
        : logger_ ( logger  )
        , level_  ( level   )
        , context_( context )
    {}

    template<class T, class... U>
    void operator()( const T &  t, U&&...  u )
    {
        std::string  fmt = std::string( "[{}] " ) + t;
        logger_.log(
            level_,
            fmt.c_str(),
            context_.name(),
            std::forward<U>( u )... );
    }

private:
    spdlog::logger &           logger_;
    spdlog::level::level_enum  level_;
    const log_context &        context_;
};

} // namespace logging

#define LOGGER ::logging::instance()

#define CHECK_LEVEL( level_name ) \
    LOGGER.should_log( ::spdlog::level::level_name )

#define CHECK_AND_LOG( level_name )      \
    if ( !CHECK_LEVEL( level_name ) ) {} \
    else                                 \
        ::logging::log_statement(        \
            LOGGER,                      \
            ::spdlog::level::level_name, \
            __log_context__::context() )

#define LOG_TRACE CHECK_AND_LOG( trace )
#define LOG_DEBUG CHECK_AND_LOG( debug )
#define LOG_INFO CHECK_AND_LOG( info )
#define LOG_WARNING CHECK_AND_LOG( warn )
#define LOG_ERROR CHECK_AND_LOG( err )
#define LOG_CRITICAL CHECK_AND_LOG( critical )

#define LOG_CONTEXT( name_ )                        \
    struct __log_context__                          \
    {                                               \
        static ::logging::log_context context()     \
        {                                           \
            return ::logging::log_context( name_ ); \
        }                                           \
    }

LOG_CONTEXT( "global" );

Where I'm stuck is building the stack of contexts to use when defining an inner-most __log_context__. We may use a differently-named structure and macro convention to add 1 or 2 levels (e.g. LOG_MODULE can define a __log_module__), but I want a more general solution. Here are the restrictions I can think of to make things easier:

  1. Scope nesting level may be reasonably bounded, but the user should not have to provide the current level/code may be moved to a different scope without being changed. Maybe 16 levels is enough (that gives us orgname::app::module::subsystem::subsubsystem::detail::impl::detail::util with some room to spare...)
  2. Number of next-level scopes within a scope (in a single translation unit) may be bounded, but should be much larger than the value for 1. Maybe 256 is reasonable, but I'm sure someone will have a counterexample.
  3. Ideally the same macro could be used for any context.

I have considered the following approaches:

  1. using __parent_context__ = __log_context__; struct __log_context__ ...

    hoping that __parent_context__ picks up the outer context, but I was getting compiler errors indicating that a type name must refer unambiguously to a single type in the same scope.

  2. Tracking the structures applicable to a scope in something like boost::mpl::vector

    The examples in the tutorial lead me to believe I would run into the same issue as in 1, since the vector after being pushed to needs to be given a distinct name which would need to be referred to specifically in nested scopes.

  3. Generating the name of the applicable outer scope with a preprocessor counter.

    This would work in my simple usage example above, but would fail in the presence of discontinuous declarations in a namespace or method definitions outside of the corresponding class.

Can you provide an approach?

Aucun commentaire:

Enregistrer un commentaire