目录
1.为什么要了解make_format_args?
在前面详解讲解了spdlog日志库的用法和使用注意事项,知道spdlog使用起来是非常方便的,并且各方面扩展都很容易,如下面的代码:
std::tuple<int, char> t = { 1, 'a' };
UVLOG_INFO("print std::tuple content is:{}", t);
UVLOG_INFO调用SPDLOG_LOGGER_CALL:
#define UVLOG_INFO(...) SPDLOG_LOGGER_CALL(currentLogger(), spdlog::level::info, __VA_ARGS__)
一步步往下追溯:
#define SPDLOG_LOGGER_CALL(logger, level, ...) (logger)->log(spdlog::source_loc{__FILE__, __LINE__, SPDLOG_FUNCTION}, level, __VA_ARGS__)
namespace spdlog {
class SPDLOG_API logger
{
public:
// Empty logger
explicit logger(std::string name)
: name_(std::move(name))
, sinks_()
{}
。。。
template<typename... Args>
void log_(source_loc loc, level::level_enum lvl, string_view_t fmt, Args &&... args)
{
bool log_enabled = should_log(lvl);
bool traceback_enabled = tracer_.enabled();
if (!log_enabled && !traceback_enabled)
{
return;
}
SPDLOG_TRY
{
memory_buf_t buf;
#ifdef SPDLOG_USE_STD_FORMAT
fmt_lib::vformat_to(std::back_inserter(buf), fmt, fmt_lib::make_format_args(std::forward<Args>(args)...));
#else
// seems that fmt::detail::vformat_to(buf, ...) is ~20ns faster than fmt::vformat_to(std::back_inserter(buf),..)
fmt::detail::vformat_to(buf, fmt, fmt::make_format_args(std::forward<Args>(args)...));
#endif
details::log_msg log_msg(loc, name_, lvl, string_view_t(buf.data(), buf.size()));
log_it_(log_msg, log_enabled, traceback_enabled);
}
SPDLOG_LOGGER_CATCH(loc)
}
};
最终定位到了:
fmt::detail::vformat_to(buf, fmt, fmt::make_format_args(std::forward<Args>(args)...));
spdlog利用fmt::detail::vformat_to系统函数把外面传入的参数格式化输出到buf中,此时fmt::make_format_args就起到了至关重要的作用,它的作用就是用于创建一个格式化参数包(format arguments pack)。这个参数包可以被 fmt 库的格式化函数(如 fmt::print
、fmt::format
等)使用,以实现类型安全的格式化字符串输出。
2.make_format_args的使用
fmt::make_format_args是用于构建格式化参数对象的工具函数。它们的主要作用是将一组不同类型的参数打包成一个 std::format_args对象,这些对象可以在运行时动态地传递给格式化函数,如 fmt::vformat_to、fmt::print或fmt::println。
通俗的讲fmt::make_format_args用于生成一个包含多个格式化参数的对象,这样就使得开发者可以更加灵活地处理动态数量和类型的格式化参数,特别是在需要在运行时决定格式化内容的情况下,提供了极大的便利。例如,在实现通用日志记录功能时,可以使用 fmt::make_format_args
将多个日志参数打包,然后统一传递给日志输出函数进行格式化和记录。
如:
#include <format>
#include <iostream>
#include <string_view>
// 日志记录函数,接受格式字符串和格式化参数
void log_message(std::string_view format_str, fmt::format_args args) {
std::string formatted = fmt::vformat(format_str, args);
std::cout << formatted << std::endl;
}
int main() {
int error_code = 404;
std::string_view error_message = "Not Found";
// 使用 fmt::make_format_args 打包参数
log_message("Error {}: {}", fmt::make_format_args(error_code, error_message));
return 0;
}
输出:
Error 404: Not Found
fmt::make_format_args
将error_code
和error_message
打包成一个 fmt::format_args
对象。fmt::vformat
根据格式字符串"Error {}: {}"
和打包的参数生成最终的格式化字符串。
3.make_format_args的底层原理
以下面代码为例:
double const d = 78.2555;
int x = 10;
UVLOG_INFO("output: {:e}{}", d, x);
首先看一下make_format_args的定义:
template <typename Context = format_context, typename... Args>
constexpr auto make_format_args(Args&&... args)
-> format_arg_store<Context, remove_cvref_t<Args>...> {
return {std::forward<Args>(args)...};
}
所以:
template <typename Context = format_context, typename... Args>
constexpr auto make_format_args(Args&&... args)
-> format_arg_store<Context, double, int>
{
return {78.2555, 10};
}
那么是用{78.2555, 10}这个参数包,
来初始化一个(注意,就是一个)format_arg_store<Context, double, int>
注意,Context是默认的类型:format_context,定义为:
template <typename Char>
using buffer_context =
basic_format_context<detail::buffer_appender<Char>, Char>;
using format_context = buffer_context<char>;
具体是:
basic_format_context<detail::buffer_appender<char>, char>
但需要注意的是,它在参数列表里不是最后一个。一般默认类型都是最后一个。可能跟后面的参数是变长有关
这个函数是把参数args生成format_arg_store
注意remove_cvref_t<Args>...,它的意思是把每个参数都给去掉const等信息,然后用逗号分隔:
remove_cvref_t<Args0>, remove_cvref_t<Args1>, remove_cvref_t<Args2>, ...
比如args展开是const double, int&, const std::string
remove_cvref_t<Args>...的结果就是double, int, std::string
下面看一下format_arg_store的定义:
/**
\rst
An array of references to arguments. It can be implicitly converted into
`~fmt::basic_format_args` for passing into type-erased formatting functions
such as `~fmt::vformat`.
\endrst
*/
template <typename Context, typename... Args>
class format_arg_store
#if FMT_GCC_VERSION && FMT_GCC_VERSION < 409
// Workaround a GCC template argument substitution bug.
: public basic_format_args<Context>
#endif
{
private:
static const size_t num_args = sizeof...(Args);
static const size_t num_named_args = detail::count_named_args<Args...>();
static const bool is_packed = num_args <= detail::max_packed_args;
using value_type = conditional_t<is_packed, detail::value<Context>,
basic_format_arg<Context>>;
detail::arg_data<value_type, typename Context::char_type, num_args,
num_named_args>
data_;
friend class basic_format_args<Context>;
static constexpr unsigned long long desc =
(is_packed ? detail::encode_types<Context, Args...>()
: detail::is_unpacked_bit | num_args) |
(num_named_args != 0
? static_cast<unsigned long long>(detail::has_named_args_bit)
: 0);
public:
template <typename... T>
FMT_CONSTEXPR FMT_INLINE format_arg_store(T&&... args)
:
#if FMT_GCC_VERSION && FMT_GCC_VERSION < 409
basic_format_args<Context>(*this),
#endif
data_{detail::make_arg<
is_packed, Context,
detail::mapped_type_constant<remove_cvref_t<T>, Context>::value>(
std::forward<T>(args))...} {
detail::init_named_args(data_.named_args(), 0, 0, args...);
}
};
实例化后的样子:
fmt::v8::format_arg_store<
fmt::v8::basic_format_context<fmt::v8::appender,char>,double,int>::format_arg_store
<同上>
(
const double & <args_0>,
const int & <args_1>
)
其中:
num_args = sizeof...(Args);的结果是:2
is_packed = num_args <= detail::max_packed_args;的结果是:0
data_是用detail::make_arg<
is_packed, Context,
detail::mapped_type_constant<Args, Context>::value>(args)...
来初始化的。抛开干扰因素,就是data_{detail::make_arg<>(args)...},展开就类似于:
data_{detail::make_arg<>(args0), detail::make_arg<>(args1), detail::make_arg<>(args2),...}
当前有两个参数,一个是const double&,一个是const int&
所以最后初始化data_的实际样子就是:
data_{
detail::make_arg
<
is_packed, Context,
detail::mapped_type_constant<const double&, Context>::value
>
(123.45678),
detail::make_arg
<
is_packed, Context,
detail::mapped_type_constant<int&, Context>::value
>
(10)
}
简化一下:
data_{
detail::make_arg(123.45678),
detail::make_arg(10)
}
detail::make_arg的定义,它会返回Value类对象,其实它是一个union。
刨除掉干扰代码后,如下:
template <bool IS_PACKED, typename Context, type, typename T,
FMT_ENABLE_IF(IS_PACKED)>
auto make_arg(const T& val) -> value<Context> {
const auto& arg = arg_mapper<Context>().map(val);
return {arg};
}
所以,data_最后的初始化是:
data_{value<Context>, value<Context>}
第一个参数是double类型的,所以const auto& arg = arg_mapper<Context>().map(val);返回了一个double值:
auto map(double val) -> double { return val; }
但make_arg要求返回Value<Context>类型的值,所以会用这个double来初始化这个Value。
看看Value的定义,发现是个Union
// A formatting argument value.
template <typename Context> class value {
public:
using char_type = typename Context::char_type;
union {
monostate no_value;
int int_value;
unsigned uint_value;
long long long_long_value;
unsigned long long ulong_long_value;
int128_t int128_value;
uint128_t uint128_value;
bool bool_value;
char_type char_value;
float float_value;
double double_value;
long double long_double_value;
const void* pointer;
string_value<char_type> string;
custom_value<Context> custom;
named_arg_value<char_type> named_args;
};
value(float val) : float_value(val) {}
value(double val) : double_value(val) {}
value(long double val) : long_double_value(val) {}
。。。
value(const named_arg_info<char_type>* args, size_t size)
: named_args{args, size} {}
template <typename T> value(const T& val) {
custom.value = &val;
// Get the formatter type through the context to allow different contexts
// have different extension points, e.g. `formatter<T>` for `format` and
// `printf_formatter<T>` for `printf`.
custom.format = format_custom_arg<
T, conditional_t<has_formatter<T, Context>::value,
typename Context::template formatter_type<T>,
fallback_formatter<T, char_type>>>;
}
};
注意,里面还有如下的类型:
const void* pointer;
string_value<char_type> string;
custom_value<Context> custom;
named_arg_value<char_type> named_args;
当用double类型的数据来初始化Value时:
value(double val) : double_value(val) {}
之后调用到arg_data的构造函数:
detail::make_arg<
is_packed, //第一个参数
Context, //第二个参数
detail::mapped_type_constant<Args, Context>::value //第三个参数
>(args)...
arg_data的泛化版本:
template <typename T, typename Char, size_t NUM_ARGS/*参数的个数,比如2个*/,
size_t NUM_NAMED_ARGS>
struct arg_data {
// args_[0].named_args points to named_args_ to avoid bloating format_args.
// +1 to workaround a bug in gcc 7.5 that causes duplicated-branches warning.
T args_[1 + (NUM_ARGS != 0 ? NUM_ARGS : +1)];
named_arg_info<Char> named_args_[NUM_NAMED_ARGS];
template <typename... U>
arg_data(const U&... init) : args_{T(named_args_, NUM_NAMED_ARGS), init...} {}
arg_data(const arg_data& other) = delete;
auto args() const -> const T* { return args_ + 1; }
auto named_args() -> named_arg_info<Char>* { return named_args_; }
};
其中:
named_arg_info的定义:
template <typename Char> struct named_arg_info {
const Char* name;
int id;
};
最后走的是arg_data的这个偏特化版本:
template <typename T, typename Char, size_t NUM_ARGS>
struct arg_data<T, Char, NUM_ARGS, 0> {
// +1 to workaround a bug in gcc 7.5 that causes duplicated-branches warning.
//如果参数是2个,那么就是T args_[2];,比如存放的是123.456和10,用Value的方式保存
//如果参数是3个,那么就是T args_[2];
T args_[NUM_ARGS != 0 ? NUM_ARGS : +1];
template <typename... U>
arg_data(const U&... init) : args_{init...} {} //程序走到这里
auto args() const -> const T* { return args_; }
auto named_args() -> std::nullptr_t {
return nullptr;
}
};
注意唯一的成员变量是个数组:T args_[NUM_ARGS != 0 ? NUM_ARGS : +1];
这个NUM_ARGS在前面已经用sizeof...(Args)推导出来了,是2。
注意args_{init...}的写法,一个数组可以用{}来初始化吗?答案是可以的。比如
int xxx[3] {3,6,1};
arg_data的构造函数实例化之后:
arg_data<value<Context>,char,2,0>::
arg_data<同上>
<value<Context >,value<Context > >
//由于arg_data的构造函数也是个模板,所以这里也根据传进来的变长参数给实例化了template <typename... U>
//一个是包含的double类型数据,一个是int类型数据
(
const value<Context > & <init_0>,
const value<Context > & <init_1>
)
这样的话,成员变量T args_[NUM_ARGS]
就变成了:value<Context> args_[2];
在这里,数据的类型被擦除了。
detail::arg_data<detail::value<basic_format_context<appender,char> >,char,2,0>::
arg_data<detail::value<basic_format_context<appender,char> >,char,2,0>
<detail::value<basic_format_context<appender,char> >,
detail::value<basic_format_context<appender,char> >
>
(
const detail::value<basic_format_context<appender,char> > & <init_0>,
const detail::value<basic_format_context<appender,char> > & <init_1>
)
稍作简化:
detail::arg_data<detail::value<Context >,char,2,0>::
arg_data<detail::value<Context >,char,2,0>
<detail::value<Context >,detail::value<Context > >
(
const detail::value<Context > & <init_0>,
const detail::value<Context > & <init_1>
)
解下来detail::init_named_args函数里是空的:
template <typename... Args>
void init_named_args(std::nullptr_t, int, int, const Args&...) {
}
4.总结
fmt::make_format_args函数利用了 C++ 的模板和类型推导机制,能够在编译时确定传入参数的类型,并将它们存储在一个统一的格式化参数对象中。具体来说,它们会创建一个 fmt::format_arg_store 对象,该对象内部维护了一个参数数组,每个参数都被封装为 fmt::basic_format_arg<Context> 类型。这种封装确保了每个参数在格式化时能够正确匹配相应的格式说明符,提供了类型安全的保证。
例如,当传入一个 int 类型的参数时,fmt::make_format_args 会将其存储为 int,并在格式化时根据格式说明符正确处理。如果传入的参数类型与格式说明符不匹配,编译器会在编译时发出警告或错误,避免了运行时的潜在问题。
fmt::make_format_args创建的格式化参数对象并不复制传入的参数,而是引用它们。这意味着这些对象具有引用语义,确保在格式化操作期间能够正确访问参数数据。然而,这也带来了一个重要的注意事项:程序员必须确保传入的参数在格式化对象的生命周期内保持有效。换句话说,传入的参数必须在格式化操作完成之前保持存活,否则会导致未定义行为。
例如,在以下代码中:
fmt::format_args args = fmt::make_format_args(x, y, z);
// 使用 args 进行格式化操作
变量 x
、y
和 z必须在 args
使用期间保持有效。如果它们在 args
使用之前被销毁或修改,可能会导致格式化结果不正确或程序崩溃。
fmt::make_format_args
是 fmt 库内部使用的一个工具,用于创建格式化参数包。它通常不会被用户直接调用,而是被封装在更高层次的格式化函数中。了解它的工作原理有助于深入理解 fmt 库的类型安全格式化机制,但在日常编程中,直接使用 fmt 库提供的接口会更加方便和正确。