diff --git a/README.md b/README.md index a142d31..f558dfe 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,17 @@ vcpkg install dbg-macro ## Advanced features +### Multiple arguments + +Passing multiple arguments to the `dbg()` macro would just work. The output of +`dbg(x, y)` is same as `dbg(x); dbg(y)`. If there are unprotected commas, +please wrap them with parenthesis. + +```c++ +dbg(42, "hello world"); +dbg(1, (std::vector{2, 3, 4}), 5); +``` + ### Hexadecimal, octal and binary format If you want to format integers in hexadecimal, octal or binary representation, you can diff --git a/dbg.h b/dbg.h index 3a76130..4ee01f2 100644 --- a/dbg.h +++ b/dbg.h @@ -622,40 +622,65 @@ inline bool pretty_print(std::ostream& stream, #endif +template +struct last { + using type = typename last::type; +}; + +template +struct last { + using type = T; +}; + +template +using last_t = typename last::type; + class DebugOutput { public: - DebugOutput(const char* filepath, - int line, - const char* function_name, - const char* expression) - : m_use_colorized_output(isColorizedOutputEnabled()), - m_filepath(filepath), - m_line(line), - m_function_name(function_name), - m_expression(expression) { - const std::size_t path_length = m_filepath.length(); + // Helper alias to avoid obscure type `const char* const*` in signature. + using expr_t = const char*; + + DebugOutput(const char* filepath, int line, const char* function_name) + : m_use_colorized_output(isColorizedOutputEnabled()) { + std::string path = filepath; + const std::size_t path_length = path.length(); if (path_length > MAX_PATH_LENGTH) { - m_filepath = ".." + m_filepath.substr(path_length - MAX_PATH_LENGTH, - MAX_PATH_LENGTH); + path = ".." + path.substr(path_length - MAX_PATH_LENGTH, MAX_PATH_LENGTH); + } + std::stringstream ss; + ss << ansi(ANSI_DEBUG) << "[" << path << ":" << line << " (" + << function_name << ")] " << ansi(ANSI_RESET); + m_location = ss.str(); + } + + template + auto print(std::initializer_list exprs, T&&... values) + -> last_t { + if (exprs.size() != sizeof...(values)) { + std::cerr + << m_location << ansi(ANSI_WARN) + << "The number of arguments mismatch, please check unprotected comma" + << ansi(ANSI_RESET) << std::endl; } + return print_impl(exprs.begin(), std::forward(values)...); } + private: template - T&& print(const std::string& type, T&& value) const { + T&& print_impl(const expr_t* expr, T&& value) { const T& ref = value; std::stringstream stream_value; const bool print_expr_and_type = pretty_print(stream_value, ref); std::stringstream output; - output << ansi(ANSI_DEBUG) << "[" << m_filepath << ":" << m_line << " (" - << m_function_name << ")] " << ansi(ANSI_RESET); + output << m_location; if (print_expr_and_type) { - output << ansi(ANSI_EXPRESSION) << m_expression << ansi(ANSI_RESET) - << " = "; + output << ansi(ANSI_EXPRESSION) << *expr << ansi(ANSI_RESET) << " = "; } output << ansi(ANSI_VALUE) << stream_value.str() << ansi(ANSI_RESET); if (print_expr_and_type) { - output << " (" << ansi(ANSI_TYPE) << type << ansi(ANSI_RESET) << ")"; + output << " (" << ansi(ANSI_TYPE) << type_name() << ansi(ANSI_RESET) + << ")"; } output << std::endl; std::cerr << output.str(); @@ -663,7 +688,13 @@ class DebugOutput { return std::forward(value); } - private: + template + auto print_impl(const expr_t* exprs, T&& value, U&&... rest) + -> last_t { + print_impl(exprs, std::forward(value)); + return print_impl(exprs + 1, std::forward(rest)...); + } + const char* ansi(const char* code) const { if (m_use_colorized_output) { return code; @@ -674,15 +705,13 @@ class DebugOutput { const bool m_use_colorized_output; - std::string m_filepath; - const int m_line; - const std::string m_function_name; - const std::string m_expression; + std::string m_location; static constexpr std::size_t MAX_PATH_LENGTH = 20; static constexpr const char* const ANSI_EMPTY = ""; static constexpr const char* const ANSI_DEBUG = "\x1b[02m"; + static constexpr const char* const ANSI_WARN = "\x1b[33m"; static constexpr const char* const ANSI_EXPRESSION = "\x1b[36m"; static constexpr const char* const ANSI_VALUE = "\x1b[01m"; static constexpr const char* const ANSI_TYPE = "\x1b[32m"; @@ -696,14 +725,49 @@ T&& identity(T&& t) { return std::forward(t); } +template +auto identity(T&&, U&&... u) -> last_t { + return identity(std::forward(u)...); +} + } // namespace dbg #ifndef DBG_MACRO_DISABLE -// We use a variadic macro to support commas inside expressions (e.g. -// initializer lists): -#define dbg(...) \ - dbg::DebugOutput(__FILE__, __LINE__, __func__, #__VA_ARGS__) \ - .print(dbg::type_name(), (__VA_ARGS__)) + +#define DBG_FOREACH_1(fn, x) fn(x) +#define DBG_FOREACH_2(fn, x, ...) fn(x), DBG_FOREACH_1(fn, __VA_ARGS__) +#define DBG_FOREACH_3(fn, x, ...) fn(x), DBG_FOREACH_2(fn, __VA_ARGS__) +#define DBG_FOREACH_4(fn, x, ...) fn(x), DBG_FOREACH_3(fn, __VA_ARGS__) +#define DBG_FOREACH_5(fn, x, ...) fn(x), DBG_FOREACH_4(fn, __VA_ARGS__) +#define DBG_FOREACH_6(fn, x, ...) fn(x), DBG_FOREACH_5(fn, __VA_ARGS__) +#define DBG_FOREACH_7(fn, x, ...) fn(x), DBG_FOREACH_6(fn, __VA_ARGS__) +#define DBG_FOREACH_8(fn, x, ...) fn(x), DBG_FOREACH_7(fn, __VA_ARGS__) +#define DBG_FOREACH_9(fn, x, ...) fn(x), DBG_FOREACH_8(fn, __VA_ARGS__) +#define DBG_FOREACH_10(fn, x, ...) fn(x), DBG_FOREACH_9(fn, __VA_ARGS__) +#define DBG_FOREACH_11(fn, x, ...) fn(x), DBG_FOREACH_10(fn, __VA_ARGS__) +#define DBG_FOREACH_12(fn, x, ...) fn(x), DBG_FOREACH_11(fn, __VA_ARGS__) +#define DBG_FOREACH_13(fn, x, ...) fn(x), DBG_FOREACH_12(fn, __VA_ARGS__) +#define DBG_FOREACH_14(fn, x, ...) fn(x), DBG_FOREACH_13(fn, __VA_ARGS__) +#define DBG_FOREACH_15(fn, x, ...) fn(x), DBG_FOREACH_14(fn, __VA_ARGS__) +#define DBG_FOREACH_16(fn, x, ...) fn(x), DBG_FOREACH_15(fn, __VA_ARGS__) +#define DBG_FOREACH_N(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, \ + _14, _15, _16, N, ...) \ + N + +#define DBG_FOREACH(fn, ...) \ + DBG_FOREACH_N(__VA_ARGS__, DBG_FOREACH_16, DBG_FOREACH_15, DBG_FOREACH_14, \ + DBG_FOREACH_13, DBG_FOREACH_12, DBG_FOREACH_11, \ + DBG_FOREACH_10, DBG_FOREACH_9, DBG_FOREACH_8, DBG_FOREACH_7, \ + DBG_FOREACH_6, DBG_FOREACH_5, DBG_FOREACH_4, DBG_FOREACH_3, \ + DBG_FOREACH_2, DBG_FOREACH_1, unused) \ + (fn, __VA_ARGS__) + +#define DBG_STRINGIFY_IMPL(x) #x +#define DBG_STRINGIFY(x) DBG_STRINGIFY_IMPL(x) + +#define dbg(...) \ + dbg::DebugOutput(__FILE__, __LINE__, __func__) \ + .print({DBG_FOREACH(DBG_STRINGIFY, __VA_ARGS__)}, __VA_ARGS__) #else #define dbg(...) dbg::identity(__VA_ARGS__) #endif // DBG_MACRO_DISABLE diff --git a/tests/basic.cpp b/tests/basic.cpp index fd85e46..9e1978e 100644 --- a/tests/basic.cpp +++ b/tests/basic.cpp @@ -39,6 +39,36 @@ TEST_CASE("side effects") { CHECK(x == 2); } +TEST_CASE("multiple arguments") { + SECTION("output format") { + // The output of dbg(x, y) should be same as dbg(x); dbg(y). + std::stringstream ss; + const auto orig_buf = std::cerr.rdbuf(ss.rdbuf()); + // Put multiple statements in the same line to get exactly same output. + // clang-format off + dbg(42); dbg("test"); dbg(42, "test"); + // clang-format on + std::cerr.rdbuf(orig_buf); + + std::string lines[4]; + for (int i = 0; i < 4; i++) { + std::getline(ss, lines[i]); + } + CHECK(lines[0] == lines[2]); // output for 42 + CHECK(lines[1] == lines[3]); // output for "test" + } + + SECTION("expression") { + // It should return the last expression. + int x = dbg(1, 2, 1 + 2); + CHECK(x == 3); + + // Wrap unprotected commas with parenthesis. + x = dbg(1, (std::vector{2, 3, 4}), 5); + CHECK(x == 5); + } +} + TEST_CASE("pretty_print") { SECTION("primitive types") { CHECK(pretty_print(3) == "3"); diff --git a/tests/demo.cpp b/tests/demo.cpp index b9e1fe6..74dc85f 100644 --- a/tests/demo.cpp +++ b/tests/demo.cpp @@ -63,6 +63,10 @@ int main() { dbg(9 + 33); dbg(test_string + " world"); + dbg("====== multiple arguments"); + + dbg(test_int, (std::vector{2, 3, 4}), test_string); + dbg("====== containers"); const std::vector dummy_vec_int{3, 2, 3};