Skip to content

Commit bd08c9a

Browse files
committed
Add std::format_args support; Bump version to 1.0.5
1 parent f464e36 commit bd08c9a

File tree

6 files changed

+168
-15
lines changed

6 files changed

+168
-15
lines changed

CHANGELOG

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
#
1+
# v1.0.5 - 02-10-2026
2+
- Add `std::format_args` support: pass a pre-built `std::format_args` object directly to any log call
3+
- Arguments are unpacked and copied into the log entry on the calling thread (safe across thread boundary)
4+
- Supports all standard `std::basic_format_arg` types: bool, char, int, unsigned, long long, unsigned long long, float, double, long double, const char*, string_view, and void*
5+
- Custom formatter types (`std::formatter` handle) are not supported; they will be logged as `<handle>`
6+
- Fix `const void*` handling in `enqueue_argument` (also benefits regular pointer logging)
7+
8+
#
29
- Updated cmake config template to avoid cmake config warning
310

411
# v1.0.4 - 01-11-2026

CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
cmake_minimum_required(VERSION 3.20)
22

3-
project(slick-logger
4-
VERSION 1.0.4
3+
project(slick-logger
4+
VERSION 1.0.5
55
LANGUAGES CXX)
66

77
set(CMAKE_CXX_STANDARD 20)

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,27 @@ int main() {
181181
- **Extensible**: Easy to add custom formatters for user-defined types
182182
- **Standard**: Part of C++20 standard library, no external dependencies
183183

184+
### Passing std::format_args
185+
186+
You can pass a pre-built `std::format_args` object as the single argument to any log call. This lets you capture format arguments once and reuse them, or forward a pre-built arg pack from another function:
187+
188+
```cpp
189+
int count = 42;
190+
double price = 9.99;
191+
std::string_view name = "widget";
192+
193+
// Capture args once, pass to logger
194+
auto args = std::make_format_args(count, price, name);
195+
LOG_INFO("count={} price={:.2f} name={}", args);
196+
197+
// Also works inline
198+
LOG_DEBUG("x={} y={}", std::make_format_args(x, y));
199+
```
200+
201+
> **Note:** `std::make_format_args` requires all arguments to be lvalues. Pass temporary values via a named variable.
202+
203+
> **Limitation:** Custom formatter types (types requiring a `std::formatter` specialization, represented as `handle` inside `std::format_args`) are not supported. They will be logged as `<handle>`. Use the normal variadic log call for custom-formatted types.
204+
184205
### Multi-Sink Usage
185206
186207
```cpp

include/slick/logger.hpp

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,16 @@ enum class ArgType : uint8_t {
195195
STRING_DYNAMIC // std::string - stored in separate queue
196196
};
197197

198+
// True when Args... is exactly one type convertible to std::format_args —
199+
// used to detect the pre-built format_args overload in log_to_sink.
200+
// std::make_format_args() returns an implementation-defined store type
201+
// (e.g. std::_Format_arg_store on MSVC) that converts to std::format_args,
202+
// so we check convertibility rather than exact type identity.
203+
template<typename... Args>
204+
inline constexpr bool is_single_format_args_v =
205+
sizeof...(Args) == 1 &&
206+
(std::is_convertible_v<std::decay_t<Args>, std::format_args> || ...);
207+
198208
#pragma pack(push, 1)
199209
struct StringRef {
200210
const char* ptr; // Pointer to string data
@@ -624,6 +634,8 @@ class Logger {
624634
template<typename T>
625635
void enqueue_argument(LogArgument& arg, T&& value);
626636

637+
void enqueue_format_args(LogEntry& entry, std::format_args fa);
638+
627639
StringRef store_string_in_queue(std::string_view str);
628640

629641
std::unique_ptr<slick::SlickQueue<LogEntry>> log_queue_;
@@ -1429,13 +1441,20 @@ inline void Logger::log_to_sink(int sink_index, LogLevel level, FormatT&& format
14291441
entry.sink_index = sink_index;
14301442
if constexpr (IS_STRING_LITERAL(format)) {
14311443
entry.format_ptr = format; // String literal - safe to store pointer
1432-
entry.arg_count = sizeof...(args);
14331444

1434-
// push arguments
1435-
size_t arg_idx = 0;
1436-
static_assert(sizeof...(args) <= SLICK_LOGGER_MAX_ARGS, "Too many log arguments");
1437-
(enqueue_argument(entry.args[arg_idx++], std::forward<Args>(args)), ...);
1438-
}
1445+
if constexpr (is_single_format_args_v<Args...>) {
1446+
// Pre-built std::format_args: unpack values into the entry on the
1447+
// calling thread (format_args holds non-owning references).
1448+
enqueue_format_args(entry,
1449+
std::get<0>(std::forward_as_tuple(std::forward<Args>(args)...)));
1450+
} else {
1451+
// Normal path: push individual arguments
1452+
entry.arg_count = sizeof...(args);
1453+
size_t arg_idx = 0;
1454+
static_assert(sizeof...(args) <= SLICK_LOGGER_MAX_ARGS, "Too many log arguments");
1455+
(enqueue_argument(entry.args[arg_idx++], std::forward<Args>(args)), ...);
1456+
}
1457+
}
14391458
else {
14401459
// Store dynamic string in string queue
14411460
static_assert(sizeof...(args) == 0, "Dynamic format strings are only supported when there are no arguments, to avoid dangling pointers.");
@@ -1559,6 +1578,10 @@ inline void Logger::enqueue_argument(LogArgument& arg, T&& value) {
15591578
arg.type = ArgType::STRING_DYNAMIC;
15601579
arg.value.dynamic_str = store_string_in_queue(value);
15611580
}
1581+
else if constexpr (std::is_same_v<DecayedT, const void*>) {
1582+
arg.type = ArgType::PTR;
1583+
arg.value.ptr = const_cast<void*>(value);
1584+
}
15621585
else if constexpr (std::is_pointer_v<DecayedT>) {
15631586
arg.type = ArgType::PTR;
15641587
arg.value.ptr = static_cast<void*>(value);
@@ -1590,6 +1613,29 @@ inline StringRef Logger::store_string_in_queue(std::string_view str) {
15901613
return StringRef{dest, length};
15911614
}
15921615

1616+
inline void Logger::enqueue_format_args(LogEntry& entry, std::format_args fa) {
1617+
size_t arg_idx = 0;
1618+
while (arg_idx < SLICK_LOGGER_MAX_ARGS) {
1619+
auto arg = fa.get(arg_idx);
1620+
bool is_monostate = false;
1621+
std::visit_format_arg([&]<typename T>(T&& v) {
1622+
using DT = std::decay_t<T>;
1623+
if constexpr (std::is_same_v<DT, std::monostate>) {
1624+
is_monostate = true;
1625+
} else if constexpr (std::is_same_v<DT, typename std::basic_format_arg<std::format_context>::handle>) {
1626+
// Custom type via handle: std::format_context is not publicly
1627+
// constructible, so store a placeholder instead.
1628+
enqueue_argument(entry.args[arg_idx], std::string_view{"<handle>"});
1629+
} else {
1630+
enqueue_argument(entry.args[arg_idx], std::forward<T>(v));
1631+
}
1632+
}, arg);
1633+
if (is_monostate) break;
1634+
++arg_idx;
1635+
}
1636+
entry.arg_count = static_cast<uint8_t>(arg_idx);
1637+
}
1638+
15931639
inline void Logger::shutdown(bool clear_sinks) {
15941640
if (running_.load(std::memory_order_relaxed)) {
15951641
running_.store(false, std::memory_order_release);

src/logger.hpp

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,16 @@ enum class ArgType : uint8_t {
193193
STRING_DYNAMIC // std::string - stored in separate queue
194194
};
195195

196+
// True when Args... is exactly one type convertible to std::format_args —
197+
// used to detect the pre-built format_args overload in log_to_sink.
198+
// std::make_format_args() returns an implementation-defined store type
199+
// (e.g. std::_Format_arg_store on MSVC) that converts to std::format_args,
200+
// so we check convertibility rather than exact type identity.
201+
template<typename... Args>
202+
inline constexpr bool is_single_format_args_v =
203+
sizeof...(Args) == 1 &&
204+
(std::is_convertible_v<std::decay_t<Args>, std::format_args> || ...);
205+
196206
#pragma pack(push, 1)
197207
struct StringRef {
198208
const char* ptr; // Pointer to string data
@@ -622,6 +632,8 @@ class Logger {
622632
template<typename T>
623633
void enqueue_argument(LogArgument& arg, T&& value);
624634

635+
void enqueue_format_args(LogEntry& entry, std::format_args fa);
636+
625637
StringRef store_string_in_queue(std::string_view str);
626638

627639
std::unique_ptr<slick::SlickQueue<LogEntry>> log_queue_;
@@ -1427,13 +1439,20 @@ inline void Logger::log_to_sink(int sink_index, LogLevel level, FormatT&& format
14271439
entry.sink_index = sink_index;
14281440
if constexpr (IS_STRING_LITERAL(format)) {
14291441
entry.format_ptr = format; // String literal - safe to store pointer
1430-
entry.arg_count = sizeof...(args);
14311442

1432-
// push arguments
1433-
size_t arg_idx = 0;
1434-
static_assert(sizeof...(args) <= SLICK_LOGGER_MAX_ARGS, "Too many log arguments");
1435-
(enqueue_argument(entry.args[arg_idx++], std::forward<Args>(args)), ...);
1436-
}
1443+
if constexpr (is_single_format_args_v<Args...>) {
1444+
// Pre-built std::format_args: unpack values into the entry on the
1445+
// calling thread (format_args holds non-owning references).
1446+
enqueue_format_args(entry,
1447+
std::get<0>(std::forward_as_tuple(std::forward<Args>(args)...)));
1448+
} else {
1449+
// Normal path: push individual arguments
1450+
entry.arg_count = sizeof...(args);
1451+
size_t arg_idx = 0;
1452+
static_assert(sizeof...(args) <= SLICK_LOGGER_MAX_ARGS, "Too many log arguments");
1453+
(enqueue_argument(entry.args[arg_idx++], std::forward<Args>(args)), ...);
1454+
}
1455+
}
14371456
else {
14381457
// Store dynamic string in string queue
14391458
static_assert(sizeof...(args) == 0, "Dynamic format strings are only supported when there are no arguments, to avoid dangling pointers.");
@@ -1557,6 +1576,10 @@ inline void Logger::enqueue_argument(LogArgument& arg, T&& value) {
15571576
arg.type = ArgType::STRING_DYNAMIC;
15581577
arg.value.dynamic_str = store_string_in_queue(value);
15591578
}
1579+
else if constexpr (std::is_same_v<DecayedT, const void*>) {
1580+
arg.type = ArgType::PTR;
1581+
arg.value.ptr = const_cast<void*>(value);
1582+
}
15601583
else if constexpr (std::is_pointer_v<DecayedT>) {
15611584
arg.type = ArgType::PTR;
15621585
arg.value.ptr = static_cast<void*>(value);
@@ -1588,6 +1611,29 @@ inline StringRef Logger::store_string_in_queue(std::string_view str) {
15881611
return StringRef{dest, length};
15891612
}
15901613

1614+
inline void Logger::enqueue_format_args(LogEntry& entry, std::format_args fa) {
1615+
size_t arg_idx = 0;
1616+
while (arg_idx < SLICK_LOGGER_MAX_ARGS) {
1617+
auto arg = fa.get(arg_idx);
1618+
bool is_monostate = false;
1619+
std::visit_format_arg([&]<typename T>(T&& v) {
1620+
using DT = std::decay_t<T>;
1621+
if constexpr (std::is_same_v<DT, std::monostate>) {
1622+
is_monostate = true;
1623+
} else if constexpr (std::is_same_v<DT, typename std::basic_format_arg<std::format_context>::handle>) {
1624+
// Custom type via handle: std::format_context is not publicly
1625+
// constructible, so store a placeholder instead.
1626+
enqueue_argument(entry.args[arg_idx], std::string_view{"<handle>"});
1627+
} else {
1628+
enqueue_argument(entry.args[arg_idx], std::forward<T>(v));
1629+
}
1630+
}, arg);
1631+
if (is_monostate) break;
1632+
++arg_idx;
1633+
}
1634+
entry.arg_count = static_cast<uint8_t>(arg_idx);
1635+
}
1636+
15911637
inline void Logger::shutdown(bool clear_sinks) {
15921638
if (running_.load(std::memory_order_relaxed)) {
15931639
running_.store(false, std::memory_order_release);

tests/test_logger.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class SlickLoggerTest : public ::testing::Test {
1818
std::filesystem::remove("test_char_array.log");
1919
std::filesystem::remove("test_single_string.log");
2020
std::filesystem::remove("test_empty_string.log");
21+
std::filesystem::remove("test_format_args.log");
2122
}
2223
};
2324

@@ -370,6 +371,38 @@ TEST_F(SlickLoggerTest, EmptyStringView) {
370371
EXPECT_EQ(line.find(" [INFO] Log empty string: "), 26);
371372
}
372373

374+
TEST_F(SlickLoggerTest, FormatArgsLogging) {
375+
std::filesystem::remove("test_format_args.log");
376+
377+
slick::logger::Logger::instance().init("test_format_args.log", 1024);
378+
379+
int i = 42;
380+
double d = 3.14;
381+
std::string_view sv = "hello";
382+
bool b = true;
383+
void* p = &d;
384+
385+
// Pre-build format_args and pass to logger
386+
LOG_INFO("int={} double={:.2f} str={} bool={} pointer={:p}", std::make_format_args(i, d, sv, b, p));
387+
388+
slick::logger::Logger::instance().shutdown();
389+
390+
ASSERT_TRUE(std::filesystem::exists("test_format_args.log"));
391+
392+
std::ifstream log_file("test_format_args.log");
393+
std::string file_contents;
394+
std::string line;
395+
std::getline(log_file, line); // first line is the logger's version
396+
while (std::getline(log_file, line)) {
397+
file_contents += line + "\n";
398+
}
399+
400+
EXPECT_NE(file_contents.find("int=42 double=3.14 str=hello bool=true"), std::string::npos);
401+
402+
log_file.close();
403+
std::filesystem::remove("test_format_args.log");
404+
}
405+
373406
int main(int argc, char **argv) {
374407
testing::InitGoogleTest(&argc, argv);
375408
return RUN_ALL_TESTS();

0 commit comments

Comments
 (0)