Logging Wrong Line Number

Apr 4, 2025 at 3:11pm
I'm working with C++17 or prior.

I'm writing a custom logger class (I don't have access to a logging library) and have an issue with the following use case.

In Main.cpp, Foo defines its own log function, flog in terms of the LOG_INFO macro (assume some other work is done inside flog). But since the preprocessor expands this macro where used, __LINE__ evaluates to the line inside flog (Line 25), instead of where flog actually gets used (Line 52).

I got around this problem by defining FLOG_INFO in Main.cpp but this is would be a hassle for every Logger user to define.

Is there a way to redefine flog(), Logger, or any of the Logger.h macros to get the preprocessor to use the line where flog() is called?

Logger.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#pragma once

#include <iostream>
#include <string>
using std::string;
using std::cout;

enum LogLevel
{
    info,
    warning,
    error
};

#define __FILENAME__ (strrchr(__FILE__, '\\') ? strrchr(__FILE__, '\\') + 1 : __FILE__)
#define LOG(_LEVEL, _MSG, ...) \
    log(__FILENAME__, __func__, __LINE__, _LEVEL, _MSG, __VA_ARGS__)
#define LOG_INFO(_MSG, ...) \
    log(__FILENAME__, __func__, __LINE__, LogLevel::info, _MSG, __VA_ARGS__)
#define LOG_WARN(_MSG, ...) \
    log(__FILENAME__, __func__, __LINE__, LogLevel::warning, _MSG, __VA_ARGS__)
#define LOG_ERROR(_MSG, ...) \
    log(__FILENAME__, __func__, __LINE__, LogLevel::error, _MSG, __VA_ARGS__)

struct Logger
{
    void log(string fileName, string funcName, int lineNum, 
        LogLevel loglevel, string msg)
    {
        cout << "[Date] " << fileName << " " << funcName << " Line-" << lineNum
            << " [" << ToStr(loglevel) << "] " << msg << "\n";
    }

    string ToStr(const LogLevel& level)
    {
        switch (level)
        {
        case info:
            return "INFO";
        case warning:
            return "WARN";
        case error:
            return "ERROR";
        }
    }
};


Main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream> 
#include <string>

#include "Log.h"

#define FLOG_INFO(_MSG, ...) \
    log(__FILENAME__, __func__, __LINE__, LogLevel::info, _MSG, __VA_ARGS__)

struct Foo
{
    enum Severity
    {
        Terrible,
        Bad,
        Good,
        Great
    };

    // Foo-defined log, implemented in terms of Logger::log
    void flog(Severity severity, string msg)
    {
        switch(severity)
        {
        case Terrible:
            m_log.LOG_ERROR(msg);
            break;
        case Bad:
        case Good:
            m_log.LOG_WARN(msg);
            break;
        case Great:
            m_log.LOG_INFO(msg);
            break;
        }
    }

    // Convenience functions
    void flog_terrible(string msg) { flog(Severity::Terrible, msg); }
    void flog_bad(string msg) { flog(Severity::Bad, msg); }
    void flog_good(string msg) { flog(Severity::Good, msg); }
    void flog_great(string msg) { flog(Severity::Great, msg); }

public:
    Logger m_log; 
};


int main()
{
    Foo f;
    f.m_log.LOG_INFO("Hello Alice!"); // Correct: Line 51
    f.flog(Foo::Severity::Terrible, "Hello Bob!"); // Incorrect: Line 25
    f.m_log.FLOG_INFO("Hello Bob!"); // Correct: Line 53
}


Output
[Date] ConsoleApplication1.cpp main Line-51 [INFO] Hello Alice!
[Date] ConsoleApplication1.cpp flog Line-25 [ERROR] Hello Bob!
[Date] ConsoleApplication1.cpp main Line-53 [INFO] Hello Bob!
Last edited on Apr 6, 2025 at 4:51am
Apr 4, 2025 at 3:18pm
Instead of __LINE__ have you tried std::source_location::line (C++20):
https://en.cppreference.com/w/cpp/utility/source_location/line

and the example shown.

Apr 4, 2025 at 6:05pm
The __FILE__ and __LINE__ macros expand to the file name or line number of the code location where they are expanded!

So, it's clear that you can not use them directly inside your logging function implementation.

But you can still use them inside a macro definition. And that macro definition can, very well, be inside a "shared" header file.

There is no need to re-define (copy) our "helper" macro in every source file where logging is used!


logging.h
1
2
void _my_log_impl(const char *const file_name, const int line_no, const char *const text);
#define MY_LOG(TEXT) _my_log_impl(__FILE__, __LINE__, (TEXT)) 

logging.c
1
2
3
4
5
6
7
#include "logging.h"
#include <stdio.h>

void _my_log_impl(const char *const file_name, const int line_no, const char *const text)
{
	printf("Log message received from file \"%s\" at line #%d: \"%s\"\n", file_name, line_no, text);
}

main.c
1
2
3
4
5
6
7
8
#include "logging.h"

int main()
{
	MY_LOG("foo");
	MY_LOG("bar");
	MY_LOG("baz");
}

output:
Log message received from file "c:\repos\consoleapplication40\main.c" at line #8: "foo"
Log message received from file "c:\repos\consoleapplication40\main.c" at line #9: "bar"
Log message received from file "c:\repos\consoleapplication40\main.c" at line #10: "baz"


You get the idea. Should be trivial to expand to your needs 😏
Last edited on Apr 4, 2025 at 6:57pm
Apr 6, 2025 at 5:15am
kigar64551 wrote:
But you can still use them inside a macro definition. And that macro definition can, very well, be inside a "shared" header file.

There is no need to re-define (copy) our "helper" macro in every source file where logging is used!

The technique of defining the helper macros in a header file, logging.h, makes sense. spdlog[1] does this.

One unfortunate consequence of the use of helper macros, instead of an object-oriented approach, is that if another class (struct Foo in this case) decides it wants to implement its own log-like call on top of the helper macros, see Foo::flog, then wherever Foo::flog gets called (e.g., Main.cpp, Line-52), the logged line number is incorrect.

Foo::flog is probably a bad name so allow me to clarify with this use case:

Foo.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
...

#define FLOG_INFO(_MSG, ...) \
    Report(__FILENAME__, __func__, __LINE__, LogLevel::info, _MSG, __VA_ARGS__)

struct Foo
{
    enum Severity
    {
        Terrible,
        Bad,
        Good,
        Great
    };

    // Foo users call Report() to both log *and* send the log message to another app. 
    // BUT wherever Report() gets called, __LINE__ evaluates to Line-23 below instead of where Report() is used!
    inline void Report(Severity severity, string msg)
    {
        switch(severity)
        {
        case Terrible:
            m_log.LOG_ERROR(msg); // Uses logger.h functionality
            break;
        case Bad:
        case Good:
            m_log.LOG_WARN(msg);
            break;
        case Great:
            m_log.LOG_INFO(msg);
            break;
        }

        SendLogMsgToAnotherApplication(msg); // !
    }

public:
    Logger m_log; 
};


FooUser.h
1
2
3
4
5
6
7
8
9
10
#include "Logger.h"

struct FooUser
{
    inline void DoBadWork(void)
    {
        Foo f; 
        f.Report("Bad work was done here!"); // f.Report() should have logged Line-8 but it'll log Line-23 (referring to Foo.h Line-23) instead!
    }
};


It seems the use of helper macros is unavoidable because getting the line number is functionality provided by the preprocessor (pre-C++20, as seeplus mentioned).

I'm just curious whether folks working with pre-C++20 standards design away usage like Foo::Report, or if they need to redefine another set of helper macros (as I did with #define FLOG_INFO ).


[1] https://github.com/gabime/spdlog/blob/48bcf39a661a13be22666ac64db8a7f886f2637e/include/spdlog/spdlog.h#L288
Last edited on Apr 6, 2025 at 5:40am
Apr 6, 2025 at 6:30pm
One unfortunate consequence of the use of helper macros, instead of an object-oriented approach, is that if another class (struct Foo in this case) decides it wants to implement its own log-like call on top of the helper macros, see Foo::flog, then wherever Foo::flog gets called (e.g., Main.cpp, Line-52), the logged line number is incorrect.


You can do something like this:

logging.h
1
2
3
4
5
6
#ifndef LOGGING_FUNCTION
#define LOGGING_FUNCTION logger_default
#endif

void logger_default(const char *const file_name, const int line_no, const char *const text);
define LOG(...) LOGGING_FUNCTION(__FILE__, __LINE__, __VA_ARGS__)

logging.c
1
2
3
4
void logger_default(const char *const file_name, const int line_no, const char *const text)
{
    /* default implementation */
}

user1.c
1
2
3
4
5
6
7
#include "logging.h"

void foo()
{
    LOG("Message #1");
    LOG("Message #2");
}

user2.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define LOGGING_FUNCTION my_special_logger
include "logging.h"

static void my_special_logger(const char *const file_name, const int line_no, const char *const text)
{
    const char *custom_message = /* ... */;
    logger_default(file_name, line_no, custom_message);
}

void bar()
{
    LOG("Message #1");
    LOG("Message #2");
}
Last edited on Apr 6, 2025 at 9:07pm
Apr 6, 2025 at 9:13pm
kigar64551 wrote:
#ifdef CUSTOM_LOGGER
#ifdef CUSTOM_LOGGER
# define LOG(TEXT) CUSTOM_LOGGER(__FILE__, __LINE__, (TEXT))
#else
# define LOG(TEXT) logger_default(__FILE__, __LINE__, (TEXT))
#endif
...
#define CUSTOM_LOGGER my_special_logger
include "logging.h"

static void my_special_logger(const char *const file_name, const int line_no, const char *const text)
{
const char *custom_message = /* ... */;
logger_default(file_name, line_no, custom_message);
}


Have the user specify their log-like function that takes the preprocessor macros as the mandatory args. Great idea! Thanks, Kigar!
Registered users can post here. Sign in or register to post.