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?
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!
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