Skip to content

8. User Defined Types

Odysseas Georgoudis edited this page Apr 15, 2022 · 6 revisions

Introduction

When a user defined type has to be logged, the copy constructor is called and the formatting is performed on the backend logging thread via a call to operator<<(ostream&).
This creates issues with user defined types that contain mutable references, raw pointers that can be mutated or std::shared_ptr.

By default quill performs a compile time check and will fail for such types that are not safe to copy.
Many user defined types including STL containers, tuples and pairs of build in types are automatically detected in compile type as safe to copy and will pass the check.

The following code gives a good idea of the types that by default are safe to get copied

     struct filter_copyable : disjunction<std::is_arithmetic<T>,
                                          is_string<T>,
                                          std::is_trivial<T>,
                                          is_user_defined_copyable<T>,
                                          is_copyable_pair<T>,
                                          is_copyable_tuple<T>,
                                          is_copyable_container<T>
                                          >

The following types is just a small example of detectable safe-to-copy types

    std::vector<std::vector<std::vector<int>>>;
    std::tuple<int,bool,double,float>>;
    std::pair<char, double>;
    std::tuple<std::vector<std::string>, std::map<int, std::sting>>;

Note Passing pointers for logging is not permitted by libfmt in compile time, with the only exception being void*. Therefore they are excluded from the above check.

User Defined Types Requirements

To log a user defined type the following requirements must met:

  • The user defined type has to be copy constructible
  • operator<<(ostream&) needs to be defined

Logging User Defined Types in default mode

In default mode copying non-trivial user defined types is not permitted unless they are tagged as safe to copy

Consider the following example :

    class User
    {
    public:
      User(std::string name) : name(std::move(name)){};

      friend std::ostream& operator<<(std::ostream& os, User2 const& obj)
      {
        os << "name : " << obj.name;
        return os;
      }
    private:
      std::string name; 
    };

   int main()
   {
     User user{"Hello"};
     LOG_INFO(quill::get_logger(), "The user is {}", usr);
   }

The above log statement would fail with a compiler error. The type is non-trivial, there is no way to automatically detect the type is safe to copy. In order to log this user defined type we have two options:

  1. call operator<< on the caller path and pass a std::string to the logger if the type contains mutable references and is not safe to copy.
  2. mark the type as safe to copy and let the backend logger thread do the formatting if the type is safe to copy.

Registering or tagging user defined types as safe to copy

It is possible to mark the class as safe to copy and the logger will attempt to copy it. In this case the user defined type will get copied. Note Read the FAQ and make sure the class does not contain mutable references or pointers before tagging it as safe.

There are 2 different ways to do that :

  1. Specialize copy_loggable<T>
    class User
    {
    public:
      User(std::string name) : name(std::move(name)){};

      friend std::ostream& operator<<(std::ostream& os, User2 const& obj)
      {
        os << "name : " << obj.name;
        return os;
      }

    private:
      std::string name; 
    };
    
    /** Registered as safe to copy **/
    namespace quill {
      template <>
      struct copy_loggable<User> : std::true_type { };
    }

    int main()
    {
      User user{"Hello"};
      LOG_INFO(quill::get_logger(), "The user is {}", usr);
    }
  1. Use QUILL_COPY_LOGGABLE macro inside your class definition
    class User
    {
    public:
      User(std::string name) : name(std::move(name)){};

      friend std::ostream& operator<<(std::ostream& os, User2 const& obj)
      {
        os << "name : " << obj.name;
        return os;
      }

      QUILL_COPY_LOGGABLE; /** Tagged as safe to copy **/

    private:
      std::string name; 
    };

    int main()
    {
      User user{"Hello"};
      LOG_INFO(quill::get_logger(), "The user is {}", usr);
    }

Then the following will compile, the user defined type will get copied, and operator<< will be called in the background thread.

Generally speaking, tagging functionality in this mode exists to also make the user think about the user defined type they are logging. It has to be maintained when a new class member is added. If the log level severity of the log statement is below INFO you might as well consider formatting the type to a string in the caller path instead of maintaining a safe-to-copy tag.

Logging non-copy constructible or unsafe to copy user defined types

Consider the following unsafe to copy user defined type. In this case we want to format on the caller thread.
This has to be explicitly done by the user as it might be expensive. There is a utility function quill::utility::to_string(object) or users can write their own routine.

#include "quill/Quill.h"
#include "quill/Utility.h"

class User
{
public:
  User(std::string* name) : name(name){};

  friend std::ostream& operator<<(std::ostream& os, User const& obj)
  {
    os << "name : " << obj.name;
    return os;
  }

private:
  std::string* name;
};

int main()
{
  auto str = std::make_unique<std::string>("User A");
  User usr{str.get()};

  // We format the object in the hot path because it is not safe to copy this kind of object
  LOG_INFO(quill::get_logger(), "The user is {}", quill::utility::to_string(usr));

  // std::string* is modified - Here the backend worker receives a copy of User but the pointer to
  // std::string* is still shared and mutated in the below line
  str->replace(0, 1, "T");
}

Logging in QUIL_MODE_UNSAFE

When QUIL_MODE_UNSAFE is enabled, Quill will not check in compile time for safe to copy user defined types.
All types will are copied unconditionally in this mode as long as they are copy constructible. This mode is not recommended as the user has to be extremely careful about any user define type they are logging. However, it is there for users who don't want to tag their types.

The following example compiles and copies the user defined type even tho it is a non-trivial type.

    #define QUIL_MODE_UNSAFE
    #include "quill/Quill.h"

    class User
    {
    public:
      User(std::string name) : name(std::move(name)){};

      friend std::ostream& operator<<(std::ostream& os, User2 const& obj)
      {
        os << "name : " << obj.name;
        return os;
      }
    private:
      std::string name; 
    };
   
    int main()
    {
      User user{"Hello"};
      LOG_INFO(quill::get_logger(), "The user is {}", usr);
    }