2

I am designing a logger. I'll format for it. I made a design that I would predetermine the format string and then keep this data in a tuple and print it to the log.

The code below is working now, but I want it to work in the parts I commented out. So, according to the default arguments I set, I want it to use the default argument if no argument is given.

NOTE: I want these processes to be at compile time.

#include <iostream>
#include <tuple>
#include <memory>
#include <type_traits>

class MyLogger {
public:
    template<typename... Args>
    void Log(const char* format, Args... args) {
        printf(format, args...);
        printf("\n");
    }
};

static inline std::unique_ptr<MyLogger> logger = std::make_unique<MyLogger>();

template<typename FMT, typename... Args>
struct LogLineDef {
    LogLineDef(Args... args) : values(args...) {}
    
    std::tuple<Args...> values;
};

constexpr char FMT1[] = "A:%d, B:%d, C:%d, D:%f ";
using L1 = LogLineDef<std::integral_constant<const char*, FMT1>, int, int, int, float>;

template<typename FMT, typename... Args>
void log_helper(const char* message, const LogLineDef<FMT, Args...>& logger_line_def) {
    std::apply([message](const auto&... args) {
        logger->Log((FMT::value + std::string(message)).c_str(), args...);
    }, logger_line_def.values);
}

#define LOG(logger_line_def, message, ...) log_helper(message, logger_line_def, ##__VA_ARGS__)

int main() {
    LOG(L1(1, 2, 3, 4.f), "error");
    //LOG(L1(1, 2, 3), "error"); // should be legal according to default value
    //LOG(L1(1, 2), "error"); // should be legal according to default value 
}
  • What are the default values? Just zeros or custom ones? – Evg Apr 02 '23 at 15:27
  • `L1` constructor takes 4 parameters. You are trying to pass fewer. Perhaps you meant to make `LogLineDef` constructor itself a template, with a parameter pack distinct from `Args` so you could provide fewer values there (and pad the tuple with *something*; you didn't explain what outcome you expect from `LOG(L1(1, 2, 3), "error");`). – Igor Tandetnik Apr 02 '23 at 15:27
  • @Evg I prefer it to be custom. – Enes Aygün Apr 02 '23 at 19:57
  • @Evg Even better solution would be: if the value corresponding to the formatting does not come. Never write that information on the screen. For example; LOG(L1(1, 2, 3), "error"); When I make a log call with , the screen output should be as follows: A:1, B:2, C:3 error. – Enes Aygün Apr 02 '23 at 20:07
  • @EnesAygün Your current design doesn't allow for such flexibility. You define the `printf` format up front; the code doesn't know enough about what's in it to chop it up for parts. If you want to be able to print parameters piecemeal, then somewhere in there you need a separate format string for each parameter. – Igor Tandetnik Apr 02 '23 at 23:57
  • @IgorTandetnik you are right. Actually I don't want runtime cost. I think the solution I want will cost runtime. – Enes Aygün Apr 03 '23 at 07:16

1 Answers1

1

I implemented a solution where you can define a list of types and with labels by:

static constexpr auto L1 = LabelList<int, int, int, float>{"A", "B", "C", "D"};

If you provide less arguments they are not printed. If the provides arguments have another types then in the definition you will get a compile time error. You need to define a c_str formatter for every type you want to support by:

template <>
inline constexpr char const* c_format<int> = "%d";

Here is the full logger:

#include <cstdio>
#include <memory>
#include <sstream>
#include <string>
#include <string_view>
#include <tuple>
#include <type_traits>


class MyLogger {
public:
    void Log(std::string const& data, std::string const& message) {
        std::printf("%s %s\n", data.c_str(), message.c_str());
    }
};

static inline std::unique_ptr<MyLogger> logger = std::make_unique<MyLogger>();


template <typename T>
inline constexpr char const* c_format = "c_format missing";

template <>
inline constexpr char const* c_format<int> = "%d";

template <>
inline constexpr char const* c_format<float> = "%f";


template <typename T>
struct Label: std::string_view {
    using std::string_view::string_view;
    using type = T;
};

template <typename ... Ts, std::size_t ... Is, typename U, typename ... Us>
std::string to_string(
    std::tuple<Label<Ts> ...> const& labels,
    std::index_sequence<Is ...>,
    U&& first, Us&& ... values
) {
    static_assert(
        std::is_same_v<typename std::remove_reference_t<decltype(std::get<0>(labels))>::type, U> &&
        (std::is_same_v<typename std::remove_reference_t<decltype(std::get<Is + 1>(labels))>::type, Us> && ...));

    using namespace std;
    std::ostringstream format;
    format << std::get<0>(labels) << ": " << c_format<U>;
    ((format << ", " << std::get<Is + 1>(labels) << ": " << c_format<Us>), ...);

    auto const c_format = std::move(format).str();
    auto const lenght =std::snprintf(nullptr, 0, c_format.c_str(), first, values ...);
    std::string result(lenght, '\0');
    sprintf(result.data(), c_format.c_str(), first, values ...);
    return result;
}


template <typename ... Ts>
struct LabelList: std::tuple<Label<Ts> ...> {
    using std::tuple<Label<Ts> ...>::tuple;

    template <typename ... Us>
    std::string operator()(Us&& ... values) const {
        static_assert(sizeof...(Us) <= sizeof...(Ts));

        if constexpr (sizeof...(Us) == 0) {
            return "";
        } else {
            return to_string(*this,
                std::make_index_sequence<sizeof...(Us) - 1>(),
                std::forward<Us>(values) ...);
        }
    }
};

static constexpr auto L1 = LabelList<int, int, int, float>{"A", "B", "C", "D"};


int main() {
    logger->Log(L1(1, 2, 3, 4.f), "error");
    logger->Log(L1(1, 2, 3), "error");
    logger->Log(L1(1, 2), "error");
}
A: 1, B: 2, C: 3, D: 4.000000 error
A: 1, B: 2, C: 3 error
A: 1, B: 2 error
Benjamin Buch
  • 4,752
  • 7
  • 28
  • 51
  • thank you for your response. Good answer. But, is there a way to default it with a value I give as custom? – Enes Aygün Apr 02 '23 at 19:57
  • Even better solution would be: if the value corresponding to the formatting does not come. Never write that information on the screen. For example; LOG(L1(1, 2, 3), "error"); When I make a log call with , the screen output should be as follows: A:1, B:2, C:3 error. – Enes Aygün Apr 02 '23 at 20:07
  • Note that the question is tagged with [tag:c++17]. – Evg Apr 02 '23 at 20:17
  • custom values ​​must be given with using alias. Because there can be different types and numbers of aliases. – Enes Aygün Apr 02 '23 at 20:17
  • @EnesAygün I would recommend building such a solution on top of the [`fmt`](https://github.com/fmtlib/fmt) library or if you have C++23 available (GCC 13 / LLVM 15 / MSVC 19.32 [VS 2022, 17.2]), then on [`std::format`](https://en.cppreference.com/w/cpp/utility/format/format). A logging library that currently already does this is [`spdlog`](https://github.com/gabime/spdlog). Do you want to look at `spdlog` or should I provide a solution based on `fmt` / `std::format`? – Benjamin Buch Apr 02 '23 at 20:21
  • @Evg GCC wrongly accepted it without warning in C++17 mode, I will adjust the explicit lambda template parameter list! Thanks! – Benjamin Buch Apr 02 '23 at 20:26
  • @BenjaminBuch I'm using baical P7 logger and C++17 version unfortunately. – Enes Aygün Apr 02 '23 at 20:30
  • @EnesAygün Is using the [fmt](https://github.com/fmtlib/fmt) library fine or do you need vanilla C++17? – Benjamin Buch Apr 02 '23 at 20:34
  • @BenjaminBuch pure C++ will be better. fmt lib is not used in the project. – Enes Aygün Apr 02 '23 at 20:37
  • @EnesAygün That was a lot of work, but I think the new solution can do what you wanted. I tested with GCC 12.2 and LLVM 16 in C++17 mode. – Benjamin Buch Apr 02 '23 at 21:57
  • @BenjaminBuch thanks for your response. Do you think this code costs runtime too much? – Enes Aygün Apr 03 '23 at 04:57
  • @EnesAygün If it needs "to much" depends on your requirements. ;-) You can measure the overhead by writing a [benchmark](https://github.com/google/benchmark). This implementation is not hell slow, probably somewhere in the nanosecond range per log. If your need max performance you should probably use is library like [spdlog](https://github.com/gabime/spdlog). – Benjamin Buch Apr 03 '23 at 09:35