Чем заменить __VA_ARGS__ в inline функциях C++

Есть макрос Си:

#define C_LOG(...)                                                                          \
    do {                                                                                    \
        fprintf(stdout, AC_CYAN "[INFO] %s (%d)\t " AC_RESET, __FILE__, __LINE__);          \
        fprintf(stdout, __VA_ARGS__);                                                       \
        fprintf(stdout, "\n");                                                              \
    } while (0)   

я хотел бы переписать его в inline функцию из-за перехода с Си на Си++, но столкнулся с проблемой, а именно альтернативой макроса __VA_Args__.

inline void LOG(...) {
    std::cout << AC_CYAN << "[INFO]" << __FILE__ << "(" << __LINE__ << ")" << "\t" << AC_RESET;
    std::cout << /*__VA_ARGS__?*/ << std::endl;
} 

Как мне переписать макрос, чтобы он выполнял туже самую функцию?

Заранее спасибо за ответ!


Ответы (3 шт):

Автор решения: Qwertiy

Брось эту затею

И проблема даже не в __VA_ARGS__, а в том месте, где ты выводишь __FILE__ и __LINE__. При макросе они подставляются в место вызова, а значит указывают на файл и строку, где используется C_LOG, а вот если ты переделаешь на функцию, то они всегда будут указывать на саму функцию.


А что касается передачи переменного числа параметров в функцию, то оно делается так:

template<typename... T> void f(const T&... t)

И другой вариант - использовать vfprintf - вот их пример:

void WriteFormatted(FILE *stream, const char *format, ...)
{
  va_list args;
  va_start(args, format);
  vfprintf(stream, format, args);
  va_end(args);
}
→ Ссылка
Автор решения: user7860670

В качестве замены __VA_ARGS__ можно использовать пачку параметров шаблона.

Однако вопрос плавно разросся до замены __FILE__ и __LINE__. Ньюанс в том, что эти директивы препроцессора при использовании макроса подставляются в то место, где этот макрос разворачивается, а если просто перенести их внутрь шаблонной функции, то они будут показывать расположение этой функции, а не то место, откуда она была вызвана.

¿Что же делать(1)? Для аналогичных целей в стандартной библиотеке есть специальный класс std::source_location. Однако его использовать в функции просто так не получится, так как проблема будет такой же - содержимое будет соответствовать расположению этой функции, а не тому месту, откуда она была вызвана.

¿Что же делать(2)? Что-то выполнить в месте вызова функции в С++ можно только одним способом - добавив в функцию аргумент со значеним по-умолчанию. Однако пачка параметров шаблона не может быть использована одновременно с заданием какого-то аргумента по-умолчанию.

¿Что же делать(3)? Тут можно использовать оборачивание аргумента в класс-обертку, в (implicit) конструкторе которого будет задаваться аргумент по-умолчанию. Однако подразумевается, что класс должен быть универсальным и уметь оборачивать любой аргумент, но этот класс-обертка не может быть шаблоном, так как для его неявно инстанцирования в качестве аргумента компилятору сначала необходмо будет вывести его тип, что он сможет сделать только после инстанцирования. Т.е. единственным способом его передать будет явное инстанцирование что делает этот подход по сути эквивалентым явной передаче экземпляра source_location.

¿Что же делать(4)? Вместо того, чтобы делать шаблонным весь класс-обертку, можно применить идиому type erasure и сделать шаблонным только метод печати для оборачиваемого аргумента.

#include <iostream>
#include <memory>
#include <source_location>
#include <utility>
#include <cstddef>

class
t_LocationPiggyback final
{
    friend ::std::ostream &
    operator <<
    (
        ::std::ostream &
    ,   t_LocationPiggyback const & self
    );

    private: using
    t_Print = void
    (
        ::std::ostream & output
    ,   void const * const p_value
    );

    private: template
    <
        typename x_Value
    >
    static void
    Print
    (
        ::std::ostream & output
    ,   void const * const p_value
    )
    {
        output << *static_cast<x_Value const *>(p_value);
        return;
    }

    private: void const * m_p_value;
    private: t_Print & m_print;
    private: ::std::source_location m_location;

    public: template
    <
        typename x_Value
    >
    t_LocationPiggyback
    (
        x_Value const & value
    ,   ::std::source_location location = ::std::source_location::current()
    )
    noexcept
    :   m_p_value{static_cast<void const *>(::std::addressof(value))}
    ,   m_print{Print<x_Value>}
    ,   m_location{::std::move(location)}
    {
        return;
    }

    public: auto const &
    Get_Location
    (void)
    const noexcept
    {
        return m_location;
    }
};

inline ::std::ostream &
operator <<
(
    ::std::ostream & output
,   t_LocationPiggyback const & self
)
{
    self.m_print(output, self.m_p_value);
    return output;
}

template
<
    typename... x_RestPack
>
void
Log
(
    t_LocationPiggyback first
,   x_RestPack &&... rest_pack
)
{
    auto const & location{first.Get_Location()};
    (
        (
            ::std::cout << "[INFO]" << location.file_name()
            << "(" << location.line() << ")" << "\t"
            << first
        )
        <<
        ...
        << rest_pack
    )
    << ::std::endl;
    return;
}

int
main()
{
    // тут произойдет следующее:
    // Log(t_LocationPiggyback{123, ::std::source_location::current()}, ", test"};
    Log(123, ", test");
    Log("test, ", 456);
    return 0;
}

online compiler

[INFO]/app/example.cpp(108) 123, test
[INFO]/app/example.cpp(109) test, 456

Как вариант, отказ от printf-подобия. Тем более, что вы внутри собираетесь использовать операторы вывода. Такая запись еще полезна тем, что гарантируется порядок вычисления операндов в цепочке <<, в отличии от порядка вычисления аргументов функции.

#include <iostream>
#include <source_location>

[[nodiscard]] inline auto &
Info
(
    ::std::source_location location = ::std::source_location::current()
)
{
    return ::std::cout << "[INFO]" << location.file_name()
    << "(" << location.line() << ")" << "\t";
}

int
main()
{
    Info() << 123 << ", test" << ::std::endl;
    Info() << "test, " << 456 << ::std::endl;
    return 0;
}

online compiler

→ Ссылка
Автор решения: HolyBlackCat

Самое лучшее, что можно сделать - это обертка над std::print/std::format. Так:

#include <concepts>
#include <print>
#include <source_location>
#include <type_traits>
#include <utility>

template <typename ...P>
struct FmtString
{
    std::format_string<P...> format;
    std::source_location loc;
    template <typename T>
    requires std::constructible_from<std::format_string<P...>, T &&>
    consteval FmtString(T &&format, std::source_location loc = std::source_location::current())
        : format(std::forward<T>(format)),
        loc(loc)
    {}
};

template <typename ...P>
void Log(std::type_identity_t<FmtString<P...>> format, P &&... params)
{
    // Или два вызова `std::print`, должно быть пошустрее.
    std::print("file:{}, line:{}, text:{}", format.loc.file_name(), format.loc.line(), std::format(format.format, std::forward<P>(params)...));
}

int main()
{
    Log("x = {}", 42); // file:/app/example.cpp, line:28, text:x = 42
}

Заворачиваем std::format_string в свой класс, чтобы добавить информацию о месте вызова.

→ Ссылка