-2

I was recently having some "undefined reference" errors that I managed to resolve but I don't understand why the solution works. I have the following main source file:

Main.cpp:

 #include <iostream>
#include "Log.h"

    int main()
    {
        std::cout << "Hello World!" << std::endl;

        Log log;
        log.SetLevel(Log::LevelWarning);
        log.Error("Hello!");
        log.Warning("Hello!");
        log.Info("Hello!");

        std::cin.get();
    }

which references a class declared in a separate source file:

Log.cpp:

#include <iostream>

class Log
{
public:
    enum Level
    {
       LevelError, LevelWarning, LevelInfo 
    };

private:
    Level m_LogLevel = LevelInfo;

public:
    void SetLevel (Level level)
    {
        m_LogLevel = level;
    }

    void Error (const char* message)
    {
        if (m_LogLevel >= LevelError)
            std::cout << "[ERROR]: " << message << std::endl;
    }

    void Warning (const char* message)
    {
        if (m_LogLevel >= LevelWarning)
            std::cout << "[WARNING]: " << message << std::endl;
    }

    void Info (const char* message)
    {
        if (m_LogLevel >= LevelInfo)
            std::cout << "[INFO]: " << message << std::endl;
    }
};

Log.h:

#pragma once

class Log
{
public:
    enum Level { LevelError, LevelWarning, LevelInfo };

private:
    Level m_LogLevel;

public:
    void SetLevel (Level);
    void Error (const char*);
    void Warning (const char*);
    void Info (const char*);
};

The code above gives me the linker errors "undefined reference to Log::..." for all members of the class Log being called in Main.cpp. Searching around I eventually found comment saying something along the lines of "static members and functions should be initialized", which gave me the idea of adding the following:

void Init()
{
    Log log;
    log.SetLevel(Log::LevelInfo);
    log.Error("NULL");
    log.Warning("NULL");
    log.Info("NULL");
}

To my Log.cpp file. This amazingly solves the issue and the project builds successfully, but these members are not declared as static and so I don't understand why this works, or even if this is the correct solution.

I'm using gcc in linux and compiling with "g++ Main.cpp Log.cpp -o main". Source files are in the same folder.

Matheus Leão
  • 417
  • 4
  • 12
  • Will someone explain to me why there are so many questions here related to creating loggers? Do people have nothing better to do with their time? –  Oct 04 '18 at 22:57
  • 1
    The first two snippets are so wrong it's not even funny. I'd say just revisit your chapter on creating a class with an interface. – George Oct 04 '18 at 22:58
  • The logger is an example code provided by Cherno in his C++ youtube series. – Matheus Leão Oct 04 '18 at 22:59
  • There is a header file, I just pasted the signatures directly to Main.cpp to make it simpler to read here, but the effect is the same. – Matheus Leão Oct 04 '18 at 23:00
  • In your case `Log.cpp` just "declares" a class. No code will be generated for any part of that class unless you use it. Your question is duplicate of this one: https://stackoverflow.com/questions/1410563/what-is-the-difference-between-a-definition-and-a-declaration – fukanchik Oct 04 '18 at 23:01
  • 1
    @MatheusLeão The effect is not the same. Pasting the signatures into `main.cpp` makes it impossible to diagnose the problem. – George Oct 04 '18 at 23:01
  • My understanding is that #include does exactly that: pastes the contents of a header onto a source, so why is it not the same? And why would I need to include the header in Log.cpp? – Matheus Leão Oct 04 '18 at 23:03
  • Forget about `#include`. problem is in the `Log.cpp` you are just "declaring" the class `Log`. Not defining it. If you include complete `Log.cpp` into your main.cpp and remove extra class declaration from `main.cpp` it will work. – fukanchik Oct 04 '18 at 23:05
  • @fukanchik I know that moving the whole of Log.cpp to Main.cpp works, this was just an exercise to test the use of a class defined elsewhere. Thank you for the response, I will look into declaring vs defining. – Matheus Leão Oct 04 '18 at 23:11
  • @fukanchik Log.cpp does define a class. (But it defines a different class than the one in Log.h, which is a violation of the One Definition Rule.) Maybe you're trying to say that all the member functions in Log.cpp are inline? – aschepler Oct 04 '18 at 23:11
  • 1
    The contents of `Log.cpp`, and `Log.h` after its include-d into `main.cpp` are going to blatantly violate the One Definition Rule and, as such, is undefined behavior. The shown code is fundamentally broken. – Sam Varshavchik Oct 04 '18 at 23:12
  • Can you provide an example of the correct way to reference a class from a separate source file? – Matheus Leão Oct 04 '18 at 23:17
  • 1
    As a Canadian I find the above comments about loggers offensive. – user4581301 Oct 04 '18 at 23:33
  • For types One Definition Rule only works within a single translation unit. Otherwise it won't be possible to define classes in header files. So it does not apply here. – fukanchik Oct 04 '18 at 23:59

2 Answers2

3

c++ is not java or c#. This construct won't generate any code at all:

class X
{
public:
     void foo()
     {
         std::cout << "Hello, world"<< std::endl;
     }
};

Yes, in java after compiling that you will get X.class which you can use. However in c++ this does not produce anything.

proof:

#include <stdio.h>

class X
{
    void foo()
    {
        printf("X");
    }
};

$ gcc -S main.cpp
$ cat main.s
    .file   "main.cpp"
    .ident  "GCC: (GNU) 4.9.3"
    .section        .note.GNU-stack,"",@progbits

In c++ you need something other than "definitions" for anything to be compiled.

If you want to emulate java-like compiler behaviour do this:

class X
{
public:
    void foo();
};

void X::foo()
{
    std::cout << "Hello, world"<< std::endl;
}

this will generate object file containing void X::foo().

proof:

$ gcc -c test.cpp
$ nm --demangle test.o
0000000000000000 T X::foo()

Another option is of course use inline method as you do but in this case you would need to #include whole "Log.cpp" into your "Main.cpp".

In c++ compilation is done by "translation units" instead of classes. One unit (say .cpp) produces one object file (.o). Such object file contains machine instructions and data.

Compiler does not see anything outside of a translation unit being compiled now.

Thereofre, unlike Java when main.cpp is compiled compiler only sees what is #included into main.cpp and main.cpp itself. Hence the compiler do not see the contents of Log.cpp at this time.

It's only at link time object files generated from translation units are merged together. But at this time it's too late to compile anything.

A class with inline function (like the first example) does not define any machine instructions or data.

For inline members of class machine instructions will be generated only when you use them.

Since you use your class members in main.cpp which is outside of translation unit Log.cpp during compilation of Log.cpp compiler does not generate any machine instructions for them.

Problem of One Definition Rule is a different one.

fukanchik
  • 2,811
  • 24
  • 29
  • Not sure exactly what you do mean at that part (non-inline function definition?), but "statements" is not the correct term. `std::cout << "Hello, world" << std::endl;` and `printf("X");` are statements in all your examples. – aschepler Oct 04 '18 at 23:37
  • Okay. (Though technically definitions are also declarations, and a class definition is a kind of definition, but good enough for informal purposes.) – aschepler Oct 04 '18 at 23:51
2

Your code is not correctly organized. You should not have two different class Log { ... }; contents for the same class.

Main.cpp needs to know the contents of class Log, so the (single) definition of class Log will need to be in your header file. That leaves the question of the definitions of the class member functions. There are three ways to define a class member function:

  1. Inside the class definition (which is in a header).

This is what you attempted in your Log.cpp file. If you define all the members in the class definition in Log.h, then you don't need a Log.cpp file at all.

  1. Outside the class definition, with the inline keyword, in the header file.

This would look like:

// Log.h
class Log
{
    // ...
public:
    void SetLevel(Level level);
    // ...
};

inline void Log::SetLevel(Level level)
{
    m_LogLevel = level;
}
  1. Outside the class definition, with no inline keyword, in the source file.

This would look like:

// Log.h
class Log
{
    // ...
public:
    void SetLevel(Level level);
    // ...
};

// Log.cpp
#include "Log.h"

void Log::SetLevel(Level level)
{
    m_LogLevel = level;
}

Note Log.cpp includes Log.h, so that the compiler sees the class definition before you start trying to define its members.

You are allowed to mix and match these. Although there are no strict rules on what is best, a general guideline is that small and simple functions can go in the header file, and large and complex functions might do better in the source file. Some programmers recommend not putting any function definitions inside the class definition at all, or limiting this option to cases where the definition is very short and helps make clear what the purpose of the function is, since then the (public part of the) class definition is a summary of what the class does, not details about how it does it.

In some cases, it might be appropriate to define a class inside a source *.cpp file - but this means it can only be used from that file.

aschepler
  • 70,891
  • 9
  • 107
  • 161
  • Example 3 successfully mimics what I was trying to do. My only remaining question is then, if I use this format, there is no way for m_LogLevel to be a private variable? – Matheus Leão Oct 04 '18 at 23:53
  • @MatheusLeão That shouldn't be an issue. A member of a class has access to its other private and protected members, no matter whether that member's definition appears inside or outside the class definition. – aschepler Oct 04 '18 at 23:55