Fmt: How to avoid direct string conversion for object

Created on 3 Feb 2020  路  5Comments  路  Source: fmtlib/fmt

Hi,

I'm using spdlog 1.3.1 and attempting to log a user-defined type , in this case a libconfig::Setting object. For this reason, I can't modify the source class, so instead I have created the following function:
std::ostream& operator<<(std::ostream& os, const libconfig::Setting& setting);
This appears to compile and work when I use it from places other than spdlog. The issue I run into is when I try to log a Setting& it works its way down the spdlog code path to the fmt call:

template <typename C, typename T, typename Char = typename C::char_type> inline typename std::enable_if< is_constructible<basic_string_view<Char>, T>::value && !internal::is_string<T>::value, init<C, basic_string_view<Char>, string_type>>::type make_value(const T &val) { return basic_string_view<Char>(val); }

At this point fmt decides that the custom type can be converted to a char*, because it does have that method. Unfortunately a Setting object's char* operator only works in some situations (i.e. when the Setting contains a string) otherwise it throws an exception.

Is there a way I can force fmt to skip this is_constructible check and just fall through to use my custom function?

Most helpful comment

(In general, you should only overload operators in the same namespace as the type; otherwise, name lookup might not find it properly, as it will not always go to the global scope).

You can fix the problem by creating a fmt::formatter specialization: https://fmt.dev/latest/api.html#formatting-user-defined-types

All 5 comments

Here's a small program that illustrates the issue, compile with -lconfig++:

#include <libconfig.h++>
#include <spdlog/spdlog.h>
#include <spdlog/fmt/ostr.h>

std::ostream& operator<<(std::ostream& os, const libconfig::Setting& setting)
{
  switch (setting.getType())
  {
    case libconfig::Setting::TypeGroup:
    case libconfig::Setting::TypeList:
    case libconfig::Setting::TypeArray:
    {
      for (auto& subSetting : setting)
      {
        os << subSetting;
      }
      break;
    }
    default:
    {
      os << "Key: " << setting.getPath() << std::endl;
      break;
    }
  }
  return os;
}

std::ostream& operator<<(std::ostream& os, const libconfig::Config& cfg)
{
  os << cfg.getRoot();
  return os;
}

int main()
{
  libconfig::Config cfg;
  cfg.getRoot().add("myInt", libconfig::Setting::TypeInt) = 9;
  cfg.getRoot().add("myString", libconfig::Setting::TypeString) = "str";
  libconfig::Setting& intSetting = cfg.lookup("myInt");
  libconfig::Setting& strSetting = cfg.lookup("myString");
  spdlog::info("Config, {}", cfg);
  spdlog::info("IntSetting, {}", intSetting);
  spdlog::info("IntSetting, {}", strSetting);
}

When run:

[2020-02-04 09:45:33.358] [info] Config, Key: myInt
Key: myString

[* LOG ERROR *] [2020-02-04 09:45:33] [] SettingTypeException
[2020-02-04 09:45:33.359] [info] IntSetting, str

Even though I've defined std::ostream& operator<<(std::ostream& os, const libconfig::Setting& setting) for Setting, it instead calls the operator char*, which only works if the Setting is of type Setting::TypeString.

How can I force it to call the ostream conversion function instead of the operator?

(In general, you should only overload operators in the same namespace as the type; otherwise, name lookup might not find it properly, as it will not always go to the global scope).

You can fix the problem by creating a fmt::formatter specialization: https://fmt.dev/latest/api.html#formatting-user-defined-types

Hi,

Thanks very much for your response, I hadn't considered that approach. Unfortunately I can't seem to make even the trivial implementation work. Here's what I've tried:

#include <libconfig.h++>
#include <spdlog/spdlog.h>
#include <iostream>
#include <string>


namespace fmt
{
template <>
struct formatter<libconfig::Setting>
{
  template <typename FormatContext>
  auto format(const libconfig::Setting& setting, FormatContext& ctx)
  {
    return format_to(ctx.out(), "{}", setting.getPath());
  }
};
}

int main()
{
  libconfig::Config cfg;
  cfg.getRoot().add("myInt", libconfig::Setting::TypeInt) = 9;
  cfg.getRoot().add("myString", libconfig::Setting::TypeString) = "str";
  libconfig::Setting& intSetting = cfg.lookup("myInt");
  try
  {
    std::string s = fmt::format("{}", intSetting);
    std::cout << s << std::endl;
  }
  catch (const libconfig::SettingTypeException& e)
  {
    std::cout << "Caught " << e.what() << " at " << e.getPath() << std::endl;
  }
}

And I get the output:

Caught SettingTypeException at myInt

Any idea what I'm doing wrong here?

Should be addressed by https://github.com/fmtlib/fmt/commit/1f1b50707c8f56095481e29594b0da24431f82d5. Previously a conversion to const char* was preferred but not any more, so your formatter specialization should now work.

That's great, thanks!

Was this page helpful?
0 / 5 - 0 ratings